]> eyrie.org Git - kerberos/krb5-strength.git/blob - plugin/sqlite.c
Add support for SQLite dictionaries
[kerberos/krb5-strength.git] / plugin / sqlite.c
1 /*
2  * Check a SQLite database for a password within edit distance one.
3  *
4  * This file implements yet another variation on dictionary lookups.
5  * Passwords are checked against a SQLite database (generally created with the
6  * krb5-strength-wordlist utility) that holds words and reversed words, and
7  * all passwords within edit distance one of a word in the database are
8  * rejected.
9  *
10  * To find passwords within edit distance one, this algorithm checks, for each
11  * dictionary word, whether the length of longest common prefix plus the
12  * length of the longest common suffix between that word and the password is
13  * within 1 of the length of the password.  It will be one less if a letter
14  * has been removed or replaced, and equal if the password is an exact match.
15  *
16  * To do this, the SQLite database contains one row for each dictionary word,
17  * containing both the word and the reversed version of the word.  The
18  * password is divided into two components, a prefix and a suffix.  It is
19  * checked against all dictionary words that fall lexicographically between
20  * the prefix and the prefix with its last character incremented, and then
21  * against all words where the word reversed falls lexicographically between
22  * the suffix reversed and the suffix reversed with its last character
23  * incremented.
24  *
25  * If the password matches a dictionary word, the edit must either be in the
26  * first half of the password or the last half of the password.  If in the
27  * first half, the word it will match will fall in the prefix range.  If in
28  * the last half, the word it will match will fall in the suffix range.
29  *
30  * Written by Russ Allbery <eagle@eyrie.org>
31  * Based on work by David Mazières
32  * Copyright 2014
33  *     The Board of Trustees of the Leland Stanford Junior University
34  *
35  * See LICENSE for licensing terms.
36  */
37
38 #include <config.h>
39 #include <portable/kadmin.h>
40 #include <portable/krb5.h>
41 #include <portable/system.h>
42
43 #ifdef HAVE_SQLITE3_H
44 # include <sqlite3.h>
45 #endif
46
47 #include <plugin/internal.h>
48 #include <util/macros.h>
49
50 /*
51  * The prefix and suffix SQLite query.  Finds all candidate words in range of
52  * the prefix or suffix.  The prefix query should get bind variables for the
53  * prefix and the prefix with the last character incremented; the suffix query
54  * gets the same, but the suffix should be reversed.
55  */
56 #define PREFIX_QUERY \
57     "SELECT password, drowssap FROM passwords WHERE password BETWEEN ? AND ?;"
58 #define SUFFIX_QUERY \
59     "SELECT password, drowssap FROM passwords WHERE drowssap BETWEEN ? AND ?;"
60
61
62 /*
63  * Stub for strength_init_sqlite if not built with SQLite support.
64  */
65 #ifndef HAVE_SQLITE3
66 krb5_error_code
67 strength_init_sqlite(krb5_context ctx, krb5_pwqual_moddata data UNUSED)
68 {
69     char *path = NULL;
70
71     /* Get CDB dictionary path from krb5.conf. */
72     strength_config_string(ctx, "password_dictionary_sqlite", &path);
73
74     /* If it was set, report an error, since we don't have CDB support. */
75     if (path == NULL)
76         return 0;
77     free(path);
78     krb5_set_error_message(ctx, KADM5_BAD_SERVER_PARAMS, "SQLite dictionary"
79                            " requested but not built with SQLite support");
80     return KADM5_BAD_SERVER_PARAMS;
81 }
82 #endif
83
84
85 /* Skip the rest of this file if SQLite is not available. */
86 #ifdef HAVE_SQLITE3
87
88 /*
89  * Report a SQLite error.  Takes the SQLite error code and the Kerberos
90  * context, stores the resulting error in the Kerberos context, and returns
91  * the generic KADM5_FAILURE code, since there doesn't appear to be anything
92  * better.
93  */
94 static krb5_error_code
95 error_sqlite(krb5_context ctx, int status, const char *format, ...)
96 {
97     va_list args;
98     ssize_t length;
99     char *message;
100     const char *errstr;
101     
102     errstr = sqlite3_errstr(status);
103     va_start(args, format);
104     length = vasprintf(&message, format, args);
105     va_end(args);
106     if (length < 0)
107         return strength_error_system(ctx, "cannot allocate memory");
108     krb5_set_error_message(ctx, KADM5_FAILURE, "%s: %s", message, errstr);
109     free(message);
110     return KADM5_FAILURE;
111 }
112
113
114 /*
115  * Given a string, returns a reversed version of that string in newly
116  * allocated memory.  The caller is responsible for freeing.  Returns NULL on
117  * memory allocation failure.
118  */
119 static char *
120 reverse_string(const char *string)
121 {
122     size_t length, i;
123     char *reversed;
124
125     length = strlen(string);
126     reversed = malloc(length + 1);
127     if (reversed == NULL)
128         return NULL;
129     reversed[length] = '\0';
130     for (i = 0; i < length; i++)
131         reversed[length - i - 1] = string[i];
132     return reversed;
133 }
134
135
136 /*
137  * Given two strings, return the length of their common prefix, not counting
138  * the nul character that terminates either string.
139  */
140 static size_t
141 common_prefix_length(const char *a, const char *b)
142 {
143     size_t i;
144
145     for (i = 0; a[i] == b[i] && a[i] != '\0' && b[i] != '\0'; i++)
146         ;
147     return i;
148 }
149
150
151 /*
152  * Given the length of the password, the password, the reversed password, and
153  * an executed SQLite statement that contains the word and reversed word as
154  * the first two column texts, determine whether this password is a match
155  * within edit distance one.
156  *
157  * It will be a match if the length of the common prefix of the passwod and
158  * word plus the length of the common prefix of the reversed password and the
159  * reversed word (which is the length of the common suffix) is within 1 of the
160  * length of the password.
161  */
162 static bool
163 match(size_t length, const char *password, const char *drowssap,
164       sqlite3_stmt *query)
165 {
166     const char *word, *drow;
167     size_t prefix_length, suffix_length;
168
169     word = (const char *) sqlite3_column_text(query, 0);
170     drow = (const char *) sqlite3_column_text(query, 1);
171     prefix_length = common_prefix_length(password, word);
172     if (prefix_length == length)
173         return true;
174     suffix_length = common_prefix_length(drowssap, drow);
175     return (length - prefix_length - suffix_length <= 1);
176 }
177
178
179 /*
180  * Initialize the SQLite dictionary.  Opens the database and compiles the two
181  * queries that we'll use.  Returns 0 on success, non-zero on failure (and
182  * sets the error in the Kerberos context).
183  */
184 krb5_error_code
185 strength_init_sqlite(krb5_context ctx, krb5_pwqual_moddata data)
186 {
187     char *path = NULL;
188     int status;
189
190     /* Get SQLite dictionary path from krb5.conf. */
191     strength_config_string(ctx, "password_dictionary_sqlite", &path);
192
193     /* If there is no configured dictionary, nothing to do. */
194     if (path == NULL)
195         return 0;
196
197     /* Open the database. */
198     status = sqlite3_open_v2(path, &data->sqlite, SQLITE_OPEN_READONLY, NULL);
199     if (status != 0)
200         return error_sqlite(ctx, status, "cannot open dictionary %s", path);
201
202     /* Precompile the queries we'll use. */
203     status = sqlite3_prepare_v2(data->sqlite, PREFIX_QUERY, -1,
204                                 &data->prefix_query, NULL);
205     if (status != 0)
206         return error_sqlite(ctx, status, "cannot prepare prefix query");
207     status = sqlite3_prepare_v2(data->sqlite, SUFFIX_QUERY, -1,
208                                 &data->suffix_query, NULL);
209     if (status != 0)
210         return error_sqlite(ctx, status, "cannot prepare suffix query");
211
212     /* Finished.  Return success. */
213     return 0;
214 }
215
216
217 /*
218  * Given a password, look for a word in the database within edit distance one.
219  * The full algorithm used here is described in the comment at the start of
220  * this file.  Returns a Kerberos status code, which will be KADM5_PASS_Q_DICT
221  * if the password was found in the dictionary.
222  */
223 krb5_error_code
224 strength_check_sqlite(krb5_context ctx, krb5_pwqual_moddata data,
225                       const char *password)
226 {
227     krb5_error_code code;
228     size_t length, prefix_length, suffix_length;
229     char *prefix = NULL;
230     char *drowssap = NULL;
231     bool found = false;
232     int status;
233
234     /* If we have no dictionary, there is nothing to do. */
235     if (data->sqlite == NULL)
236         return 0;
237
238     /*
239      * Determine the length of the prefix and suffix into which we'll divide
240      * the string.  Passwords shorter than two characters cannot be
241      * meaningfully checked using this method and cause boundary condition
242      * problems.
243      */
244     length = strlen(password);
245     if (length < 2)
246         return 0;
247     prefix_length = length / 2;
248     suffix_length = length - prefix_length;
249
250     /* Obtain the reversed password, used for suffix checks. */
251     drowssap = reverse_string(password);
252     if (drowssap == NULL)
253         return strength_error_system(ctx, "cannot allocate memory");
254
255     /* Set up the query for prefix matching. */
256     prefix = strdup(password);
257     if (prefix == NULL) {
258         code = strength_error_system(ctx, "cannot allocate memory");
259         goto fail;
260     }
261     status = sqlite3_bind_text(data->prefix_query, 1, password, prefix_length,
262                                NULL);
263     if (status != SQLITE_OK) {
264         code = error_sqlite(ctx, status, "cannot bind prefix start");
265         goto fail;
266     }
267     prefix[prefix_length - 1]++;
268     status = sqlite3_bind_text(data->prefix_query, 2, prefix, prefix_length,
269                                NULL);
270     if (status != SQLITE_OK) {
271         code = error_sqlite(ctx, status, "cannot bind prefix end");
272         goto fail;
273     }
274
275     /*
276      * Do prefix matching.  Get the set of all database entries starting with
277      * the same prefix and, for each, check whether our password matches that
278      * entry within edit distance one.
279      */
280     while ((status = sqlite3_step(data->prefix_query)) == SQLITE_ROW)
281         if (match(length, password, drowssap, data->prefix_query)) {
282             found = true;
283             break;
284         }
285     if (status != SQLITE_DONE && status != SQLITE_ROW) {
286         code = error_sqlite(ctx, status, "error searching by password prefix");
287         goto fail;
288     }
289     status = sqlite3_reset(data->prefix_query);
290     if (status != SQLITE_OK) {
291         code = error_sqlite(ctx, status, "error resetting prefix query");
292         goto fail;
293     }
294     if (found)
295         goto found;
296
297     /* Set up the query for suffix matching. */
298     status = sqlite3_bind_text(data->suffix_query, 1, drowssap, suffix_length,
299                                SQLITE_TRANSIENT);
300     if (status != SQLITE_OK) {
301         code = error_sqlite(ctx, status, "cannot bind suffix start");
302         goto fail;
303     }
304     drowssap[prefix_length - 1]++;
305     status = sqlite3_bind_text(data->suffix_query, 2, drowssap, suffix_length,
306                                SQLITE_TRANSIENT);
307     drowssap[prefix_length - 1]--;
308     if (status != SQLITE_OK) {
309         code = error_sqlite(ctx, status, "cannot bind suffix end");
310         goto fail;
311     }
312
313     /*
314      * Do suffix matching.  Get the set of all database entries starting with
315      * the same prefix and, for each, check whether our password matches that
316      * entry within edit distance one.
317      */
318     while ((status = sqlite3_step(data->suffix_query)) == SQLITE_ROW)
319         if (match(length, password, drowssap, data->suffix_query)) {
320             found = true;
321             break;
322         }
323     if (status != SQLITE_DONE && status != SQLITE_ROW) {
324         code = error_sqlite(ctx, status, "error searching by password suffix");
325         goto fail;
326     }
327     status = sqlite3_reset(data->suffix_query);
328     if (status != SQLITE_OK) {
329         code = error_sqlite(ctx, status, "error resetting suffix query");
330         goto fail;
331     }
332     if (found)
333         goto found;
334
335     /* No match.  Clean up and return success. */
336     memset(prefix, 0, length);
337     memset(drowssap, 0, length);
338     free(prefix);
339     free(drowssap);
340     return 0;
341
342 found:
343     /* We found the password in the dictionary. */
344     code = strength_error_dict(ctx, ERROR_DICT);
345
346 fail:
347     memset(prefix, 0, length);
348     memset(drowssap, 0, length);
349     free(prefix);
350     free(drowssap);
351     return code;
352 }
353
354
355 /*
356  * Free internal SQLite state and close the SQLite database.
357  */
358 void
359 strength_close_sqlite(krb5_context ctx UNUSED, krb5_pwqual_moddata data)
360 {
361     if (data->prefix_query != NULL)
362         sqlite3_finalize(data->prefix_query);
363     if (data->suffix_query != NULL)
364         sqlite3_finalize(data->suffix_query);
365     if (data->sqlite != NULL)
366         sqlite3_close_v2(data->sqlite);
367 }
368
369 #endif /* HAVE_CDB */