]> eyrie.org Git - kerberos/krb5-strength.git/blob - plugin/general.c
Move CDB initialization into plugin/cdb.c
[kerberos/krb5-strength.git] / plugin / general.c
1 /*
2  * The general entry points for password strength checking.
3  *
4  * Provides the strength_init, strength_check, and strength_close entry points
5  * for doing password strength checking.  These are the only interfaces that
6  * are called by the implementation-specific code, and all other checks are
7  * wrapped up in those interfaces.
8  *
9  * Developed by Derrick Brashear and Ken Hornstein of Sine Nomine Associates,
10  *     on behalf of Stanford University.
11  * Extensive modifications by Russ Allbery <rra@stanford.edu>
12  * Copyright 2006, 2007, 2009, 2012, 2013
13  *     The Board of Trustees of the Leland Stanford Junior Unversity
14  *
15  * See LICENSE for licensing terms.
16  */
17
18 #include <config.h>
19 #include <portable/kadmin.h>
20 #include <portable/krb5.h>
21 #include <portable/system.h>
22
23 #ifdef HAVE_CDB_H
24 # include <cdb.h>
25 #endif
26 #include <ctype.h>
27 #include <errno.h>
28
29 #include <plugin/internal.h>
30 #include <util/macros.h>
31
32 /* If not built with CDB support, provide some stubs. */
33 #ifndef HAVE_CDB
34 # define strength_check_cdb(c, d, p) 0
35 # define strength_close_cdb(c, d)    /* empty */
36 #endif
37
38
39 /*
40  * Load a boolean option from Kerberos appdefaults.  Takes the Kerberos
41  * context, the section name, the option, and the result location.
42  *
43  * The stupidity of rewriting the realm argument into a krb5_data is required
44  * by MIT Kerberos.
45  */
46 static void
47 default_boolean(krb5_context ctx, const char *section, const char *opt,
48                 bool *result)
49 {
50     int tmp;
51     char *realm = NULL;
52     krb5_error_code code;
53 #ifdef HAVE_KRB5_REALM
54     krb5_const_realm rdata = realm;
55 #else
56     krb5_data realm_struct;
57     const krb5_data *rdata;
58 #endif
59
60     /* Get the default realm.  This is annoying for MIT Kerberos. */
61     code = krb5_get_default_realm(ctx, &realm);
62     if (code != 0)
63         realm = NULL;
64 #ifdef HAVE_KRB5_REALM
65     rdata = realm;
66 #else
67     if (realm == NULL)
68         rdata = NULL;
69     else {
70         rdata = &realm_struct;
71         realm_struct.magic = KV5M_DATA;
72         realm_struct.data = (void *) realm;
73         realm_struct.length = strlen(realm);
74     }
75 #endif
76
77     /*
78      * The MIT version of krb5_appdefault_boolean takes an int * and the
79      * Heimdal version takes a krb5_boolean *, so hope that Heimdal always
80      * defines krb5_boolean to int or this will require more portability work.
81      */
82     krb5_appdefault_boolean(ctx, section, rdata, opt, *result, &tmp);
83     *result = tmp;
84 }
85
86
87 /*
88  * Load a number option from Kerberos appdefaults.  Takes the Kerberos
89  * context, the section name, the option, and the result location.  The native
90  * interface doesn't support numbers, so we actually read a string and then
91  * convert.
92  */
93 static void
94 default_number(krb5_context ctx, const char *section, const char *opt,
95                long *result)
96 {
97     char *tmp = NULL;
98     char *realm = NULL;
99     char *end;
100     long value;
101     krb5_error_code code;
102 #ifdef HAVE_KRB5_REALM
103     krb5_const_realm rdata = realm;
104 #else
105     krb5_data realm_struct;
106     const krb5_data *rdata;
107 #endif
108
109     /* Get the default realm.  This is annoying for MIT Kerberos. */
110     code = krb5_get_default_realm(ctx, &realm);
111     if (code != 0)
112         realm = NULL;
113 #ifdef HAVE_KRB5_REALM
114     rdata = realm;
115 #else
116     if (realm == NULL)
117         rdata = NULL;
118     else {
119         rdata = &realm_struct;
120         realm_struct.magic = KV5M_DATA;
121         realm_struct.data = (void *) realm;
122         realm_struct.length = strlen(realm);
123     }
124 #endif
125
126     /* Obtain the string from [appdefaults]. */
127     krb5_appdefault_string(ctx, section, rdata, opt, "", &tmp);
128
129     /*
130      * If we found anything, convert it to a number.  Currently, we ignore
131      * errors here.
132      */
133     if (tmp != NULL && tmp[0] != '\0') {
134         errno = 0;
135         value = strtol(tmp, &end, 10);
136         if (errno == 0 && *end == '\0')
137             *result = value;
138     }
139     if (tmp != NULL)
140         krb5_free_string(ctx, tmp);
141 }
142
143
144 /*
145  * Load a string option from Kerberos appdefaults.  Takes the Kerberos
146  * context, the section name, the realm, the option, and the result location.
147  *
148  * This requires an annoying workaround because one cannot specify a default
149  * value of NULL with MIT Kerberos, since MIT Kerberos unconditionally calls
150  * strdup on the default value.  There's also no way to determine if memory
151  * allocation failed while parsing or while setting the default value, so we
152  * don't return an error code.
153  */
154 static void
155 default_string(krb5_context ctx, const char *section, const char *opt,
156                char **result)
157 {
158     char *value = NULL;
159     char *realm = NULL;
160     krb5_error_code code;
161 #ifdef HAVE_KRB5_REALM
162     krb5_const_realm rdata;
163 #else
164     krb5_data realm_struct;
165     const krb5_data *rdata;
166 #endif
167
168     /* Get the default realm.  This is annoying for MIT Kerberos. */
169     code = krb5_get_default_realm(ctx, &realm);
170     if (code != 0)
171         realm = NULL;
172 #ifdef HAVE_KRB5_REALM
173     rdata = realm;
174 #else
175     if (realm == NULL)
176         rdata = NULL;
177     else {
178         rdata = &realm_struct;
179         realm_struct.magic = KV5M_DATA;
180         realm_struct.data = (void *) realm;
181         realm_struct.length = strlen(realm);
182     }
183 #endif
184
185     /* Obtain the string from [appdefaults]. */
186     krb5_appdefault_string(ctx, section, rdata, opt, "", &value);
187
188     /* If we got something back, store it in result. */
189     if (value != NULL) {
190         if (value[0] == '\0')
191             free(value);
192         else {
193             if (*result != NULL)
194                 free(*result);
195             *result = strdup(value);
196             krb5_free_string(ctx, value);
197         }
198     }
199
200     /* Free the realm if we got one. */
201     if (realm != NULL)
202         krb5_free_default_realm(ctx, realm);
203 }
204
205
206 /*
207  * Initialize the module.  Ensure that the dictionary file exists and is
208  * readable and store the path in the module context.  Returns 0 on success,
209  * non-zero on failure.  This function returns failure only if it could not
210  * allocate memory or internal Kerberos calls that shouldn't fail do.
211  *
212  * The dictionary file should not include the trailing .pwd extension.
213  * Currently, we don't cope with a NULL dictionary path.
214  */
215 krb5_error_code
216 strength_init(krb5_context ctx, const char *dictionary,
217               krb5_pwqual_moddata *moddata)
218 {
219     krb5_pwqual_moddata data = NULL;
220     char *cdb_path = NULL;
221     krb5_error_code code;
222
223     /* Allocate our internal data. */
224     data = calloc(1, sizeof(*data));
225     if (data == NULL)
226         return strength_error_system(ctx, "cannot allocate memory");
227     data->cdb_fd = -1;
228
229     /* Get minimum length information from krb5.conf. */
230     default_number(ctx, "krb5-strength", "minimum_length", &data->min_length);
231
232     /* Get character class restrictions from krb5.conf. */
233     default_boolean(ctx, "krb5-strength", "require_ascii_printable",
234                     &data->ascii);
235     default_boolean(ctx, "krb5-strength", "require_non_letter",
236                     &data->nonletter);
237
238     /* Use dictionary if given, otherwise get from krb5.conf. */
239     if (dictionary == NULL)
240         default_string(ctx, "krb5-strength", "password_dictionary",
241                        &data->dictionary);
242     else {
243         data->dictionary = strdup(dictionary);
244         if (data->dictionary == NULL) {
245             code = strength_error_system(ctx, "cannot allocate memory");
246             goto fail;
247         }
248     }
249
250     /* Get CDB dictionary path from krb5.conf. */
251     default_string(ctx, "krb5-strength", "password_dictionary_cdb", &cdb_path);
252
253     /* If there is no dictionary, abort our setup with an error. */
254     if (data->dictionary == NULL && cdb_path == NULL) {
255         code = KADM5_MISSING_CONF_PARAMS;
256         krb5_set_error_message(ctx, code, "password_dictionary not configured"
257                                " in krb5.conf");
258         goto fail;
259     }
260
261     /* If there is a CrackLib dictionary, initialize CrackLib. */
262     if (data->dictionary != NULL) {
263         code = strength_init_cracklib(ctx, data);
264         if (code != 0)
265             goto fail;
266     }
267
268     /* If there is a CDB dictionary, initialize TinyCDB. */
269     if (cdb_path != NULL) {
270         code = strength_init_cdb(ctx, data, cdb_path);
271         if (code != 0)
272             goto fail;
273     }
274
275     /* Initialized.  Set moddata and return. */
276     *moddata = data;
277     return 0;
278
279 fail:
280     if (data != NULL)
281         strength_close(ctx, data);
282     free(cdb_path);
283     *moddata = NULL;
284     return code;
285 }
286
287
288 /*
289  * Check if a password contains only printable ASCII characters.
290  */
291 static bool
292 only_printable_ascii(const char *password)
293 {
294     const char *p;
295
296     for (p = password; *p != '\0'; p++)
297         if (!isascii((unsigned char) *p) || !isprint((unsigned char) *p))
298             return false;
299     return true;
300 }
301
302
303 /*
304  * Check if a password contains only letters and spaces.
305  */
306 static bool
307 only_alpha_space(const char *password)
308 {
309     const char *p;
310
311     for (p = password; *p != '\0'; p++)
312         if (!isalpha((unsigned char) *p) && *p != ' ')
313             return false;
314     return true;
315 }
316
317
318 /*
319  * Check a given password.  Takes a Kerberos context, our module data, the
320  * password, the principal the password is for, and a buffer and buffer length
321  * into which to put any failure message.
322  */
323 krb5_error_code
324 strength_check(krb5_context ctx UNUSED, krb5_pwqual_moddata data,
325                const char *password, const char *principal)
326 {
327     char *user, *p;
328     const char *q;
329     size_t i, j;
330     char c;
331     krb5_error_code code;
332
333     /* Check minimum length first, since that's easy. */
334     if ((long) strlen(password) < data->min_length)
335         return strength_error_tooshort(ctx, ERROR_SHORT);
336
337     /*
338      * If desired, check whether the password contains non-ASCII or
339      * non-printable ASCII characters.
340      */
341     if (data->ascii && !only_printable_ascii(password))
342         return strength_error_generic(ctx, ERROR_ASCII);
343
344     /*
345      * If desired, ensure the password has a non-letter (and non-space)
346      * character.  This requires that people using phrases at least include a
347      * digit or punctuation to make phrase dictionary attacks or dictionary
348      * attacks via combinations of words harder.
349      */
350     if (data->nonletter && only_alpha_space(password))
351         return strength_error_class(ctx, ERROR_LETTER);
352
353     /*
354      * We get the principal (in krb5_unparse_name format) and we want to be
355      * sure that the password doesn't match the username, the username
356      * reversed, or the username with trailing digits.  We therefore have to
357      * copy the string so that we can manipulate it a bit.
358      */
359     if (strcasecmp(password, principal) == 0)
360         return strength_error_generic(ctx, ERROR_USERNAME);
361     user = strdup(principal);
362     if (user == NULL)
363         return strength_error_system(ctx, "cannot allocate memory");
364
365     /* Strip the realm off of the principal. */
366     for (p = user; p[0] != '\0'; p++) {
367         if (p[0] == '\\' && p[1] != '\0') {
368             p++;
369             continue;
370         }
371         if (p[0] == '@') {
372             p[0] = '\0';
373             break;
374         }
375     }
376
377     /*
378      * If the length of the password matches the length of the local portion
379      * of the principal, check for exact matches or reversed matches.
380      */
381     if (strlen(password) == strlen(user)) {
382         if (strcasecmp(password, user) == 0) {
383             free(user);
384             return strength_error_generic(ctx, ERROR_USERNAME);
385         }
386
387         /* Check against the reversed username. */
388         for (i = 0, j = strlen(user) - 1; i < j; i++, j--) {
389             c = user[i];
390             user[i] = user[j];
391             user[j] = c;
392         }
393         if (strcasecmp(password, user) == 0) {
394             free(user);
395             return strength_error_generic(ctx, ERROR_USERNAME);
396         }
397     }
398
399     /*
400      * If the length is greater, check whether the user just added trailing
401      * digits to the local portion of the principal to form the password.
402      */
403     if (strlen(password) > strlen(user))
404         if (strncasecmp(password, user, strlen(user)) == 0) {
405             q = password + strlen(user);
406             while (isdigit((unsigned char) *q))
407                 q++;
408             if (*q == '\0') {
409                 free(user);
410                 return strength_error_generic(ctx, ERROR_USERNAME);
411             }
412         }
413     free(user);
414
415     /* Check the password against CrackLib if it is configured. */
416     if (data->dictionary != NULL) {
417         code = strength_check_cracklib(ctx, data, password);
418         if (code != 0)
419             return code;
420     }
421
422     /* Check the password against CDB if it is configured. */
423     if (data->have_cdb) {
424         code = strength_check_cdb(ctx, data, password);
425         if (code != 0)
426             return code;
427     }
428     return 0;
429 }
430
431
432 /*
433  * Cleanly shut down the password strength plugin.  The only thing we have to
434  * do is free the memory allocated for our internal data.
435  */
436 void
437 strength_close(krb5_context ctx UNUSED, krb5_pwqual_moddata data)
438 {
439     if (data != NULL) {
440         strength_close_cdb(ctx, data);
441         free(data->dictionary);
442         free(data);
443     }
444 }