From 586c9fc81a3e2f3966109895a8c1c1857b4b3f6a Mon Sep 17 00:00:00 2001 From: Russ Allbery Date: Tue, 25 Mar 2014 00:09:39 -0700 Subject: [PATCH] Add support for SQLite dictionaries MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit The krb5-strength plugin and heimdal-strength program now support a SQLite password dictionary. This format of dictionary can detect any password within edit distance one of a dictionary word, meaning that the dictionary word can be formed by adding, removing, or changing a single character in the password. A SQLite password dictionary can be used alone or in combination with any of the other supported dictionary types. SQLite dictionary support is based on work by David Mazières. --- Makefile.am | 7 +- NEWS | 9 + README | 71 +++--- configure.ac | 5 + plugin/general.c | 15 +- plugin/internal.h | 29 ++- plugin/sqlite.c | 369 +++++++++++++++++++++++++++++++ tests/data/passwords/sqlite.json | 62 ++++++ tests/data/wordlist | 1 + tests/plugin/heimdal-t.c | 42 +++- tests/plugin/mit-t.c | 60 ++++- tests/tools/heimdal-strength-t | 36 ++- tools/heimdal-strength.pod | 35 ++- 13 files changed, 682 insertions(+), 59 deletions(-) create mode 100644 plugin/sqlite.c create mode 100644 tests/data/passwords/sqlite.json diff --git a/Makefile.am b/Makefile.am index 7fce813..c2286f9 100644 --- a/Makefile.am +++ b/Makefile.am @@ -58,7 +58,8 @@ moduledir = $(libdir)/krb5/plugins/pwqual module_LTLIBRARIES = plugin/strength.la plugin_strength_la_SOURCES = plugin/cdb.c plugin/classes.c plugin/config.c \ plugin/cracklib.c plugin/error.c plugin/general.c plugin/heimdal.c \ - plugin/internal.h plugin/mit.c plugin/principal.c plugin/vector.c + plugin/internal.h plugin/mit.c plugin/principal.c plugin/sqlite.c \ + plugin/vector.c plugin_strength_la_LDFLAGS = -module -avoid-version if EMBEDDED_CRACKLIB plugin_strength_la_LIBADD = cracklib/libcracklib.la @@ -72,8 +73,8 @@ bin_PROGRAMS = tools/heimdal-strength tools_heimdal_strength_CFLAGS = $(AM_CFLAGS) tools_heimdal_strength_SOURCES = plugin/cdb.c plugin/classes.c \ plugin/config.c plugin/cracklib.c plugin/error.c plugin/general.c \ - plugin/internal.h plugin/principal.c plugin/vector.c \ - tools/heimdal-strength.c + plugin/internal.h plugin/principal.c plugin/sqlite.c \ + plugin/vector.c tools/heimdal-strength.c if EMBEDDED_CRACKLIB tools_heimdal_strength_LDADD = cracklib/libcracklib.la else diff --git a/NEWS b/NEWS index 96758be..20726e5 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,15 @@ krb5-strength 3.0 (unreleased) + The krb5-strength plugin and heimdal-strength program now support a + SQLite password dictionary. This format of dictionary can detect any + password within edit distance one of a dictionary word, meaning that + the dictionary word can be formed by adding, removing, or changing a + single character in the password. A SQLite password dictionary can be + used alone or in combination with any of the other supported + dictionary types. SQLite dictionary support is based on work by David + Mazières. + cdbmake-wordlist has been renamed to krb5-strength-wordlist. Generating CDB dictionaries now requires the -c option; see the documentation for more information. A SQLite database of dictionary diff --git a/README b/README index 3ad8765..dbe519e 100644 --- a/README +++ b/README @@ -248,19 +248,28 @@ CONFIGURATION file is located): krb5-strength = { - password_dictionary = /path/to/cracklib/dictionary - password_dictionary_cdb = /path/to/cdb/dictionary.cdb + password_dictionary = /path/to/cracklib/dictionary + password_dictionary_cdb = /path/to/cdb/dictionary.cdb + password_dictionary_sqlite = /path/to/sqlite/dictionary.sqlite } - The first setting configures a CrackLib dictionary and the second a CDB - dictionary. The provided path should be the full path to the dictionary - files, omitting the trailing *.hwm, *.pwd, and *.pwi extensions for the - CrackLib dictionary. You can use either or both settings. If you use - both, CrackLib will be checked first, and then CDB. When checking a CDB - database, the password, the password with the first character removed, - the last character removed, the first and last characters removed, the - first two characters removed, and the last two characters removed will - all be checked against the dictionary. + The first setting configures a CrackLib dictionary, the second a CDB + dictionary, and the third a SQLite dictionary. The provided path should + be the full path to the dictionary files, omitting the trailing *.hwm, + *.pwd, and *.pwi extensions for the CrackLib dictionary. You can use + any combination of the three settings. If you use more than one, + CrackLib will be checked first, then CDB, and then SQLite as + appropriate. + + When checking against a CDB database, the password, the password with + the first character removed, the last character removed, the first and + last characters removed, the first two characters removed, and the last + two characters removed will all be checked against the dictionary. + + When checking a SQLite database, the password will be rejected if it is + within edit distance one of any word in the dictionary, meaning that the + database word can be formed from the password by deleting, adding, or + changing a single character. Then, for the external password checking program, add a new section (or modify the existing [password_quality] section) to look like the @@ -334,19 +343,28 @@ CONFIGURATION [appdefaults] section: krb5-strength = { - password_dictionary = /path/to/cracklib/dictionary - password_dictionary_cdb = /path/to/cdb/dictionary.cdb + password_dictionary = /path/to/cracklib/dictionary + password_dictionary_cdb = /path/to/cdb/dictionary.cdb + password_dictionary_sqlite = /path/to/sqlite/dictionary.sqlite } - The first setting configures a CrackLib dictionary and the second a CDB - dictionary. The provided path should be the full path to the dictionary - files, omitting the trailing *.hwm, *.pwd, and *.pwi extensions for the - CrackLib dictionary. You can use either or both settings. If you use - both, CrackLib will be checked first, and then CDB. When checking a CDB - database, the password, the password with the first character removed, - the last character removed, the first and last characters removed, the - first two characters removed, and the last two characters removed will - all be checked against the dictionary. + The first setting configures a CrackLib dictionary, the second a CDB + dictionary, and the third a SQLite dictionary. The provided path should + be the full path to the dictionary files, omitting the trailing *.hwm, + *.pwd, and *.pwi extensions for the CrackLib dictionary. You can use + any combination of the three settings. If you use more than one, + CrackLib will be checked first, then CDB, and then SQLite as + appropriate. + + When checking against a CDB database, the password, the password with + the first character removed, the last character removed, the first and + last characters removed, the first two characters removed, and the last + two characters removed will all be checked against the dictionary. + + When checking a SQLite database, the password will be rejected if it is + within edit distance one of any word in the dictionary, meaning that the + database word can be formed from the password by deleting, adding, or + changing a single character. The second option is to use the normal dict_path setting. In the [realms] section of your krb5.conf kdc.conf, under the appropriate realm @@ -368,10 +386,11 @@ CONFIGURATION dictionary matching. You can also mix and match these settings, by using dict_path for the - CrackLib dictionary path and krb5.conf for the CDB dictionary path. If - both settings are used, krb5.conf overrides the dict_path setting (so - that dict_path can be used for other password quality modules). There - is no way to specify a CDB dictionary via the dict_path setting. + CrackLib dictionary path and krb5.conf for the CDB or SQLite dictionary + paths. If both settings are used for the CrackLib path, krb5.conf + overrides the dict_path setting (so that dict_path can be used for other + password quality modules). There is no way to specify a CDB or SQLite + dictionary via the dict_path setting. Other Settings diff --git a/configure.ac b/configure.ac index be9468d..89bcc9e 100644 --- a/configure.ac +++ b/configure.ac @@ -51,6 +51,11 @@ AC_CHECK_DECLS([krb5_kt_free_entry], [], [], [RRA_INCLUDES_KRB5]) AC_LIBOBJ([krb5-extra]) RRA_LIB_KRB5_RESTORE +dnl Temporary hack to force building with SQLite. +AC_CHECK_HEADERS([sqlite3.h]) +AC_DEFINE([HAVE_SQLITE3], 1, [Define if SQLite 3 is available.]) +LIBS="$LIBS -lsqlite3" + dnl Probe for libdl, which is used for the test suite. save_LIBS="$LIBS" AC_SEARCH_LIBS([dlopen], [dl], [DL_LIBS="$LIBS"]) diff --git a/plugin/general.c b/plugin/general.c index bd8d56f..c491ea0 100644 --- a/plugin/general.c +++ b/plugin/general.c @@ -61,14 +61,17 @@ strength_init(krb5_context ctx, const char *dictionary, goto fail; /* - * Try to initialize CDB and CrackLib dictionaries. Both functions handle - * their own configuration parsing and will do nothing if the - * corresponding dictionary is not configured. + * Try to initialize CDB, CrackLib, and SQLite dictionaries. These + * functions handle their own configuration parsing and will do nothing if + * the corresponding dictionary is not configured. */ code = strength_init_cracklib(ctx, data, dictionary); if (code != 0) goto fail; code = strength_init_cdb(ctx, data); + if (code != 0) + goto fail; + code = strength_init_sqlite(ctx, data); if (code != 0) goto fail; @@ -196,11 +199,14 @@ strength_check(krb5_context ctx UNUSED, krb5_pwqual_moddata data, if (code != 0) return code; - /* Check the password against CDB and CrackLib if configured. */ + /* Check the password against CDB, CrackLib, and SQLite if configured. */ code = strength_check_cracklib(ctx, data, password); if (code != 0) return code; code = strength_check_cdb(ctx, data, password); + if (code != 0) + return code; + code = strength_check_sqlite(ctx, data, password); if (code != 0) return code; @@ -221,6 +227,7 @@ strength_close(krb5_context ctx UNUSED, krb5_pwqual_moddata data) if (data == NULL) return; strength_close_cdb(ctx, data); + strength_close_sqlite(ctx, data); last = data->rules; while (last != NULL) { tmp = last; diff --git a/plugin/internal.h b/plugin/internal.h index 545f7be..72b5a12 100644 --- a/plugin/internal.h +++ b/plugin/internal.h @@ -20,6 +20,9 @@ #ifdef HAVE_CDB_H # include #endif +#ifdef HAVE_SQLITE3_H +# include +#endif #include #ifdef HAVE_KRB5_PWQUAL_PLUGIN_H @@ -77,6 +80,11 @@ struct krb5_pwqual_moddata_st { #ifdef HAVE_CDB_H struct cdb cdb; /* Open CDB dictionary data */ #endif +#ifdef HAVE_SQLITE3_H + sqlite3 *sqlite; /* Open SQLite database handle */ + sqlite3_stmt *prefix_query; /* Query using the password prefix */ + sqlite3_stmt *suffix_query; /* Query using the reversed password suffix */ +#endif }; BEGIN_DECLS @@ -105,7 +113,7 @@ void strength_close(krb5_context, krb5_pwqual_moddata); * * If not built with CDB support, provide some stubs for check and close. * init is always a real function, which reports an error if CDB is - * requested. + * requested and not available. */ krb5_error_code strength_init_cdb(krb5_context, krb5_pwqual_moddata); #ifdef HAVE_CDB @@ -127,6 +135,25 @@ krb5_error_code strength_init_cracklib(krb5_context, krb5_pwqual_moddata, krb5_error_code strength_check_cracklib(krb5_context, krb5_pwqual_moddata, const char *password); +/* + * SQLite handling. strength_init_sqlite gets the database configuration and + * sets up the SQLite internal data, strength_check_sqlite checks a password, + * and strength_close_sqlite handles freeing resources. + * + * If not built with SQLite support, provide some stubs for check and close. + * init is always a real function, which reports an error if SQLite is + * requested and not available. + */ +krb5_error_code strength_init_sqlite(krb5_context, krb5_pwqual_moddata); +#ifdef HAVE_SQLITE3 +krb5_error_code strength_check_sqlite(krb5_context, krb5_pwqual_moddata, + const char *password); +void strength_close_sqlite(krb5_context, krb5_pwqual_moddata); +#else +# define strength_check_sqlite(c, d, p) 0 +# define strength_close_sqlite(c, d) /* empty */ +#endif + /* Check whether the password statisfies character class requirements. */ krb5_error_code strength_check_classes(krb5_context, krb5_pwqual_moddata, const char *password); diff --git a/plugin/sqlite.c b/plugin/sqlite.c new file mode 100644 index 0000000..56f8d2c --- /dev/null +++ b/plugin/sqlite.c @@ -0,0 +1,369 @@ +/* + * Check a SQLite database for a password within edit distance one. + * + * This file implements yet another variation on dictionary lookups. + * Passwords are checked against a SQLite database (generally created with the + * krb5-strength-wordlist utility) that holds words and reversed words, and + * all passwords within edit distance one of a word in the database are + * rejected. + * + * To find passwords within edit distance one, this algorithm checks, for each + * dictionary word, whether the length of longest common prefix plus the + * length of the longest common suffix between that word and the password is + * within 1 of the length of the password. It will be one less if a letter + * has been removed or replaced, and equal if the password is an exact match. + * + * To do this, the SQLite database contains one row for each dictionary word, + * containing both the word and the reversed version of the word. The + * password is divided into two components, a prefix and a suffix. It is + * checked against all dictionary words that fall lexicographically between + * the prefix and the prefix with its last character incremented, and then + * against all words where the word reversed falls lexicographically between + * the suffix reversed and the suffix reversed with its last character + * incremented. + * + * If the password matches a dictionary word, the edit must either be in the + * first half of the password or the last half of the password. If in the + * first half, the word it will match will fall in the prefix range. If in + * the last half, the word it will match will fall in the suffix range. + * + * Written by Russ Allbery + * Based on work by David Mazières + * Copyright 2014 + * The Board of Trustees of the Leland Stanford Junior University + * + * See LICENSE for licensing terms. + */ + +#include +#include +#include +#include + +#ifdef HAVE_SQLITE3_H +# include +#endif + +#include +#include + +/* + * The prefix and suffix SQLite query. Finds all candidate words in range of + * the prefix or suffix. The prefix query should get bind variables for the + * prefix and the prefix with the last character incremented; the suffix query + * gets the same, but the suffix should be reversed. + */ +#define PREFIX_QUERY \ + "SELECT password, drowssap FROM passwords WHERE password BETWEEN ? AND ?;" +#define SUFFIX_QUERY \ + "SELECT password, drowssap FROM passwords WHERE drowssap BETWEEN ? AND ?;" + + +/* + * Stub for strength_init_sqlite if not built with SQLite support. + */ +#ifndef HAVE_SQLITE3 +krb5_error_code +strength_init_sqlite(krb5_context ctx, krb5_pwqual_moddata data UNUSED) +{ + char *path = NULL; + + /* Get CDB dictionary path from krb5.conf. */ + strength_config_string(ctx, "password_dictionary_sqlite", &path); + + /* If it was set, report an error, since we don't have CDB support. */ + if (path == NULL) + return 0; + free(path); + krb5_set_error_message(ctx, KADM5_BAD_SERVER_PARAMS, "SQLite dictionary" + " requested but not built with SQLite support"); + return KADM5_BAD_SERVER_PARAMS; +} +#endif + + +/* Skip the rest of this file if SQLite is not available. */ +#ifdef HAVE_SQLITE3 + +/* + * Report a SQLite error. Takes the SQLite error code and the Kerberos + * context, stores the resulting error in the Kerberos context, and returns + * the generic KADM5_FAILURE code, since there doesn't appear to be anything + * better. + */ +static krb5_error_code +error_sqlite(krb5_context ctx, int status, const char *format, ...) +{ + va_list args; + ssize_t length; + char *message; + const char *errstr; + + errstr = sqlite3_errstr(status); + va_start(args, format); + length = vasprintf(&message, format, args); + va_end(args); + if (length < 0) + return strength_error_system(ctx, "cannot allocate memory"); + krb5_set_error_message(ctx, KADM5_FAILURE, "%s: %s", message, errstr); + free(message); + return KADM5_FAILURE; +} + + +/* + * Given a string, returns a reversed version of that string in newly + * allocated memory. The caller is responsible for freeing. Returns NULL on + * memory allocation failure. + */ +static char * +reverse_string(const char *string) +{ + size_t length, i; + char *reversed; + + length = strlen(string); + reversed = malloc(length + 1); + if (reversed == NULL) + return NULL; + reversed[length] = '\0'; + for (i = 0; i < length; i++) + reversed[length - i - 1] = string[i]; + return reversed; +} + + +/* + * Given two strings, return the length of their common prefix, not counting + * the nul character that terminates either string. + */ +static size_t +common_prefix_length(const char *a, const char *b) +{ + size_t i; + + for (i = 0; a[i] == b[i] && a[i] != '\0' && b[i] != '\0'; i++) + ; + return i; +} + + +/* + * Given the length of the password, the password, the reversed password, and + * an executed SQLite statement that contains the word and reversed word as + * the first two column texts, determine whether this password is a match + * within edit distance one. + * + * It will be a match if the length of the common prefix of the passwod and + * word plus the length of the common prefix of the reversed password and the + * reversed word (which is the length of the common suffix) is within 1 of the + * length of the password. + */ +static bool +match(size_t length, const char *password, const char *drowssap, + sqlite3_stmt *query) +{ + const char *word, *drow; + size_t prefix_length, suffix_length; + + word = (const char *) sqlite3_column_text(query, 0); + drow = (const char *) sqlite3_column_text(query, 1); + prefix_length = common_prefix_length(password, word); + if (prefix_length == length) + return true; + suffix_length = common_prefix_length(drowssap, drow); + return (length - prefix_length - suffix_length <= 1); +} + + +/* + * Initialize the SQLite dictionary. Opens the database and compiles the two + * queries that we'll use. Returns 0 on success, non-zero on failure (and + * sets the error in the Kerberos context). + */ +krb5_error_code +strength_init_sqlite(krb5_context ctx, krb5_pwqual_moddata data) +{ + char *path = NULL; + int status; + + /* Get SQLite dictionary path from krb5.conf. */ + strength_config_string(ctx, "password_dictionary_sqlite", &path); + + /* If there is no configured dictionary, nothing to do. */ + if (path == NULL) + return 0; + + /* Open the database. */ + status = sqlite3_open_v2(path, &data->sqlite, SQLITE_OPEN_READONLY, NULL); + if (status != 0) + return error_sqlite(ctx, status, "cannot open dictionary %s", path); + + /* Precompile the queries we'll use. */ + status = sqlite3_prepare_v2(data->sqlite, PREFIX_QUERY, -1, + &data->prefix_query, NULL); + if (status != 0) + return error_sqlite(ctx, status, "cannot prepare prefix query"); + status = sqlite3_prepare_v2(data->sqlite, SUFFIX_QUERY, -1, + &data->suffix_query, NULL); + if (status != 0) + return error_sqlite(ctx, status, "cannot prepare suffix query"); + + /* Finished. Return success. */ + return 0; +} + + +/* + * Given a password, look for a word in the database within edit distance one. + * The full algorithm used here is described in the comment at the start of + * this file. Returns a Kerberos status code, which will be KADM5_PASS_Q_DICT + * if the password was found in the dictionary. + */ +krb5_error_code +strength_check_sqlite(krb5_context ctx, krb5_pwqual_moddata data, + const char *password) +{ + krb5_error_code code; + size_t length, prefix_length, suffix_length; + char *prefix = NULL; + char *drowssap = NULL; + bool found = false; + int status; + + /* If we have no dictionary, there is nothing to do. */ + if (data->sqlite == NULL) + return 0; + + /* + * Determine the length of the prefix and suffix into which we'll divide + * the string. Passwords shorter than two characters cannot be + * meaningfully checked using this method and cause boundary condition + * problems. + */ + length = strlen(password); + if (length < 2) + return 0; + prefix_length = length / 2; + suffix_length = length - prefix_length; + + /* Obtain the reversed password, used for suffix checks. */ + drowssap = reverse_string(password); + if (drowssap == NULL) + return strength_error_system(ctx, "cannot allocate memory"); + + /* Set up the query for prefix matching. */ + prefix = strdup(password); + if (prefix == NULL) { + code = strength_error_system(ctx, "cannot allocate memory"); + goto fail; + } + status = sqlite3_bind_text(data->prefix_query, 1, password, prefix_length, + NULL); + if (status != SQLITE_OK) { + code = error_sqlite(ctx, status, "cannot bind prefix start"); + goto fail; + } + prefix[prefix_length - 1]++; + status = sqlite3_bind_text(data->prefix_query, 2, prefix, prefix_length, + NULL); + if (status != SQLITE_OK) { + code = error_sqlite(ctx, status, "cannot bind prefix end"); + goto fail; + } + + /* + * Do prefix matching. Get the set of all database entries starting with + * the same prefix and, for each, check whether our password matches that + * entry within edit distance one. + */ + while ((status = sqlite3_step(data->prefix_query)) == SQLITE_ROW) + if (match(length, password, drowssap, data->prefix_query)) { + found = true; + break; + } + if (status != SQLITE_DONE && status != SQLITE_ROW) { + code = error_sqlite(ctx, status, "error searching by password prefix"); + goto fail; + } + status = sqlite3_reset(data->prefix_query); + if (status != SQLITE_OK) { + code = error_sqlite(ctx, status, "error resetting prefix query"); + goto fail; + } + if (found) + goto found; + + /* Set up the query for suffix matching. */ + status = sqlite3_bind_text(data->suffix_query, 1, drowssap, suffix_length, + SQLITE_TRANSIENT); + if (status != SQLITE_OK) { + code = error_sqlite(ctx, status, "cannot bind suffix start"); + goto fail; + } + drowssap[prefix_length - 1]++; + status = sqlite3_bind_text(data->suffix_query, 2, drowssap, suffix_length, + SQLITE_TRANSIENT); + drowssap[prefix_length - 1]--; + if (status != SQLITE_OK) { + code = error_sqlite(ctx, status, "cannot bind suffix end"); + goto fail; + } + + /* + * Do suffix matching. Get the set of all database entries starting with + * the same prefix and, for each, check whether our password matches that + * entry within edit distance one. + */ + while ((status = sqlite3_step(data->suffix_query)) == SQLITE_ROW) + if (match(length, password, drowssap, data->suffix_query)) { + found = true; + break; + } + if (status != SQLITE_DONE && status != SQLITE_ROW) { + code = error_sqlite(ctx, status, "error searching by password suffix"); + goto fail; + } + status = sqlite3_reset(data->suffix_query); + if (status != SQLITE_OK) { + code = error_sqlite(ctx, status, "error resetting suffix query"); + goto fail; + } + if (found) + goto found; + + /* No match. Clean up and return success. */ + memset(prefix, 0, length); + memset(drowssap, 0, length); + free(prefix); + free(drowssap); + return 0; + +found: + /* We found the password in the dictionary. */ + code = strength_error_dict(ctx, ERROR_DICT); + +fail: + memset(prefix, 0, length); + memset(drowssap, 0, length); + free(prefix); + free(drowssap); + return code; +} + + +/* + * Free internal SQLite state and close the SQLite database. + */ +void +strength_close_sqlite(krb5_context ctx UNUSED, krb5_pwqual_moddata data) +{ + if (data->prefix_query != NULL) + sqlite3_finalize(data->prefix_query); + if (data->suffix_query != NULL) + sqlite3_finalize(data->suffix_query); + if (data->sqlite != NULL) + sqlite3_close_v2(data->sqlite); +} + +#endif /* HAVE_CDB */ diff --git a/tests/data/passwords/sqlite.json b/tests/data/passwords/sqlite.json new file mode 100644 index 0000000..f736e02 --- /dev/null +++ b/tests/data/passwords/sqlite.json @@ -0,0 +1,62 @@ +[ + { + "name": "good password", + "principal": "test@EXAMPLE.ORG", + "password": "known good password", + "code": 0 + }, + { + "name": "in dictionary", + "principal": "test@EXAMPLE.ORG", + "password": "password", + "code": "KADM5_PASS_Q_DICT", + "error": "password found in list of common passwords" + }, + { + "name": "in dictionary (longer)", + "principal": "test@EXAMPLE.ORG", + "password": "bitterbane", + "code": "KADM5_PASS_Q_DICT", + "error": "password found in list of common passwords" + }, + { + "name": "in dictionary (drop first)", + "principal": "test@EXAMPLE.ORG", + "password": "1bitterbane", + "code": "KADM5_PASS_Q_DICT", + "error": "password found in list of common passwords" + }, + { + "name": "in dictionary (drop last)", + "principal": "test@EXAMPLE.ORG", + "password": "bitterbane1", + "code": "KADM5_PASS_Q_DICT", + "error": "password found in list of common passwords" + }, + { + "name": "dictionary with three characters", + "principal": "test@EXAMPLE.ORG", + "password": "bitterbane123", + "code": 0 + }, + { + "name": "two-character dictionary word", + "principal": "test@EXAMPLE.ORG", + "password": "ab", + "code": "KADM5_PASS_Q_DICT", + "error": "password found in list of common passwords" + }, + { + "name": "three-character dictionary word", + "principal": "test@EXAMPLE.ORG", + "password": "one", + "code": "KADM5_PASS_Q_DICT", + "error": "password found in list of common passwords" + }, + { + "name": "single-character password", + "principal": "test@EXAMPLE.ORG", + "password": "a", + "code": 0 + } +] diff --git a/tests/data/wordlist b/tests/data/wordlist index 3a33db8..bc0e16e 100644 --- a/tests/data/wordlist +++ b/tests/data/wordlist @@ -1,3 +1,4 @@ +ab bitterbane happenstance one diff --git a/tests/plugin/heimdal-t.c b/tests/plugin/heimdal-t.c index b32c944..6fc5618 100644 --- a/tests/plugin/heimdal-t.c +++ b/tests/plugin/heimdal-t.c @@ -25,8 +25,8 @@ #include /* - * The password test data, generated from the JSON source. Defines an arrays - * named cdb_tests, cracklib_tests, and principal_tests. + * The password test data, generated from the JSON source. Defines arrays + * named *_tests, where * is the file name without the ".c" suffix. */ #include #include @@ -34,6 +34,7 @@ #include #include #include +#include #ifndef HAVE_KADM5_KADM5_PWCHECK_H @@ -146,14 +147,15 @@ main(void) /* * Calculate how many tests we have. There are five tests for the module * metadata and two tests per password test. We run the principal tests - * twice, once with CrackLib and once with CDB. + * three times, once each with CrackLib, CDB, and SQLite. */ count = ARRAY_SIZE(cracklib_tests); count += ARRAY_SIZE(cdb_tests); + count += ARRAY_SIZE(sqlite_tests); count += ARRAY_SIZE(classes_tests); count += ARRAY_SIZE(length_tests); count += ARRAY_SIZE(letter_tests); - count += ARRAY_SIZE(principal_tests) * 2; + count += ARRAY_SIZE(principal_tests) * 3; plan(5 + count * 2); /* Start with the krb5.conf that contains no dictionary configuration. */ @@ -236,9 +238,7 @@ main(void) bail("cannot find data/wordlist.cdb in the test suite"); setup_argv[5] = NULL; run_setup((const char **) setup_argv); - test_file_path_free(setup_argv[0]); test_file_path_free(setup_argv[4]); - test_file_path_free(path); /* Run the CDB tests. */ for (i = 0; i < ARRAY_SIZE(cdb_tests); i++) @@ -254,6 +254,36 @@ main(void) #endif /* !HAVE_CDB */ +#ifdef HAVE_SQLITE3 + + /* + * If built with SQLite, set up krb5.conf to use a SQLite dictionary + * instead. + */ + setup_argv[3] = (char *) "password_dictionary_sqlite"; + setup_argv[4] = test_file_path("data/wordlist.sqlite"); + if (setup_argv[4] == NULL) + bail("cannot find data/wordlist.sqlite in the test suite"); + setup_argv[5] = NULL; + run_setup((const char **) setup_argv); + test_file_path_free(setup_argv[0]); + test_file_path_free(setup_argv[4]); + test_file_path_free(path); + + /* Run the SQLite tests. */ + for (i = 0; i < ARRAY_SIZE(sqlite_tests); i++) + is_password_test(verifier, &sqlite_tests[i]); + for (i = 0; i < ARRAY_SIZE(principal_tests); i++) + is_password_test(verifier, &principal_tests[i]); + +#else /* !HAVE_SQLITE3 */ + + /* Otherwise, mark the SQLite tests as skipped. */ + count = ARRAY_SIZE(sqlite_tests) + ARRAY_SIZE(principal_tests); + skip_block(count * 2 + 1, "not built with SQLite support"); + +#endif /* !HAVE_SQLITE3 */ + /* Manually clean up after the results of make-krb5-conf. */ basprintf(&path, "%s/krb5.conf", tmpdir); unlink(path); diff --git a/tests/plugin/mit-t.c b/tests/plugin/mit-t.c index 7026f44..09401c3 100644 --- a/tests/plugin/mit-t.c +++ b/tests/plugin/mit-t.c @@ -26,8 +26,8 @@ #include /* - * The password test data, generated from the JSON source. Defines an arrays - * named cdb_tests, cracklib_tests, and principal_tests. + * The password test data, generated from the JSON source. Defines arrays + * named *_tests, where * is the file name without the ".c" suffix. */ #include #include @@ -35,6 +35,7 @@ #include #include #include +#include #ifndef HAVE_KRB5_PWQUAL_PLUGIN_H @@ -159,20 +160,21 @@ main(void) /* * Calculate how many tests we have. There are two tests for the module - * metadata, six more tests for initializing the plugin, and two tests per - * password test. + * metadata, seven more tests for initializing the plugin, and two tests + * per password test. * * We run all the CrackLib tests twice, once with an explicit dictionary * path and once from krb5.conf configuration. We run the principal tests - * with both CrackLib and CDB configurations. + * with CrackLib, CDB, and SQLite configurations. */ count = 2 * ARRAY_SIZE(cracklib_tests); count += ARRAY_SIZE(cdb_tests); + count += ARRAY_SIZE(sqlite_tests); count += ARRAY_SIZE(classes_tests); count += ARRAY_SIZE(length_tests); count += ARRAY_SIZE(letter_tests); - count += 2 * ARRAY_SIZE(principal_tests); - plan(2 + 6 + count * 2); + count += 3 * ARRAY_SIZE(principal_tests); + plan(2 + 7 + count * 2); /* Start with the krb5.conf that contains no dictionary configuration. */ path = test_file_path("data/krb5.conf"); @@ -326,8 +328,6 @@ main(void) setup_argv[4] = dictionary; setup_argv[5] = NULL; run_setup((const char **) setup_argv); - test_file_path_free(setup_argv[0]); - test_file_path_free(path); /* Obtain a new Kerberos context with that krb5.conf file. */ krb5_free_context(ctx); @@ -354,6 +354,48 @@ main(void) #endif /* !HAVE_CDB */ +#ifdef HAVE_SQLITE3 + + /* + * If built with SQLite, set up krb5.conf to use a SQLite dictionary + * instead. + */ + test_file_path_free(dictionary); + dictionary = test_file_path("data/wordlist.sqlite"); + if (dictionary == NULL) + bail("cannot find data/wordlist.sqlite in the test suite"); + setup_argv[3] = (char *) "password_dictionary_sqlite"; + setup_argv[4] = dictionary; + setup_argv[5] = NULL; + run_setup((const char **) setup_argv); + test_file_path_free(setup_argv[0]); + test_file_path_free(path); + + /* Obtain a new Kerberos context with that krb5.conf file. */ + krb5_free_context(ctx); + code = krb5_init_context(&ctx); + if (code != 0) + bail_krb5(ctx, code, "cannot initialize Kerberos context"); + + /* Run the SQLite and principal tests. */ + code = vtable->open(ctx, NULL, &data); + is_int(0, code, "Plugin initialization (SQLite dictionary)"); + if (code != 0) + bail("cannot continue after plugin initialization failure"); + for (i = 0; i < ARRAY_SIZE(sqlite_tests); i++) + is_password_test(ctx, vtable, data, &sqlite_tests[i]); + for (i = 0; i < ARRAY_SIZE(principal_tests); i++) + is_password_test(ctx, vtable, data, &principal_tests[i]); + vtable->close(ctx, data); + +#else /* !HAVE_SQLITE3 */ + + /* Otherwise, mark the SQLite tests as skipped. */ + count = ARRAY_SIZE(sqlite_tests) + ARRAY_SIZE(principal_tests); + skip_block(count * 2 + 1, "not built with SQLite support"); + +#endif /* !HAVE_SQLITE3 */ + /* Manually clean up after the results of make-krb5-conf. */ basprintf(&path, "%s/krb5.conf", tmpdir); unlink(path); diff --git a/tests/tools/heimdal-strength-t b/tests/tools/heimdal-strength-t index ca25815..fd83ab1 100755 --- a/tests/tools/heimdal-strength-t +++ b/tests/tools/heimdal-strength-t @@ -149,14 +149,14 @@ sub load_password_tests { # Load the password tests from JSON. Accumulate a total count of tests for # the testing plan. my (%tests, $count); -for my $type (qw(cdb classes cracklib length letter principal)) { +for my $type (qw(cdb classes cracklib length letter principal sqlite)) { my $tests = load_password_tests("$type.json"); $tests{$type} = $tests; $count += scalar(@{$tests}); } -# We run the principal tests twice, once for CrackLib and once for CDB. -$count += scalar(@{ $tests{principal} }); +# We run the principal tests three times, for CrackLib, CDB, and SQLite. +$count += 2 * scalar(@{ $tests{principal} }); # We can now calculate our plan based on three tests for each password test, # plus 21 additional tests for error handling. @@ -241,6 +241,36 @@ SKIP: { } } +# Install the krb5.conf file with configuration pointing to the SQLite +# dictionary. +my $sqlite_database = test_file_path('data/wordlist.sqlite'); +$krb5_conf = create_krb5_conf( + { + password_dictionary_sqlite => $sqlite_database, + } +); +local $ENV{KRB5_CONFIG} = $krb5_conf; + +# Check whether we were built with SQLite support. If so, run those tests. +($status, $output, $err) = run_heimdal_strength('test', 'password'); +SKIP: { + if ($status == 1 && $err =~ m{ not [ ] built [ ] with [ ] SQLite }xms) { + my $total = scalar(@{ $tests{sqlite} }); + $total += scalar(@{ $tests{principal} }); + skip('not built with SQLite support', $total * 3); + } + + # Run the SQLite and principal password tests from JSON. + note('SQLite tests'); + for my $test (@{ $tests{sqlite} }) { + check_password($test); + } + note('Generic tests with SQLite'); + for my $test (@{ $tests{principal} }) { + check_password($test); + } +} + # Test error for an unknown character class. $krb5_conf = create_krb5_conf({ require_classes => 'bogus' }); local $ENV{KRB5_CONFIG} = $krb5_conf; diff --git a/tools/heimdal-strength.pod b/tools/heimdal-strength.pod index 9e26a09..422f3a1 100644 --- a/tools/heimdal-strength.pod +++ b/tools/heimdal-strength.pod @@ -73,8 +73,8 @@ and F<*.pwi> extensions for the CrackLib dictionary. Specifies the base path to a CDB dictionary and enables CDB password dictionary lookups. The path must point to a CDB-format database whose keys are the known passwords or dictionary words. The values are ignored. -You can use the B utility to generate the CDB database -from a word list. +You can use the B utility to generate the CDB +database from a word list. The CDB dictionary lookups do not do the complex password mangling that CrackLib does. Instead, the password itself will be checked against the @@ -84,9 +84,30 @@ first two characters, and the last two characters. If any of these strings are found in the CDB database, the password will be rejected; otherwise, it will be accepted, at least by this check. -Both a CrackLib dictionary and a CDB dictionary may be configured at the -same time, in which case CrackLib will be run first, followed by the CDB -checks. +A CrackLib dictionary, a CDB dictionary, and a SQLite dictionary may all +be configured at the same time or in any combination, in which case +CrackLib will be run first, followed by CDB and then SQLite as +appropriate. + +=item password_dictionary_sqlite + +Specifies the base path to a SQLite dictionary and enables SQLite password +dictionary lookups. The path must point to a SQLite 3 database with a +table named C. This table should have two columns, C +and C, which, for each dictionary word, holds the word and the +reversed form of the word. You can use the B +utility to generate the SQLite database from a word list. + +The SQLite dictionary lookups do not do the complex password mangling that +CrackLib does, but they will detect and reject any password that is within +edit distance one of a word in the dictionary, meaning that the dictionary +word can be formed from the password by adding, deleting, or modifying a +single character. + +A CrackLib dictionary, a CDB dictionary, and a SQLite dictionary may all +be configured at the same time or in any combination, in which case +CrackLib will be run first, followed by CDB and then SQLite as +appropriate. =item require_ascii_printable @@ -163,8 +184,8 @@ Russ Allbery =head1 COPYRIGHT AND LICENSE -Copyright 2010, 2013 The Board of Trustees of the Leland Stanford Junior -University +Copyright 2010, 2013, 2014 The Board of Trustees of the Leland Stanford +Junior University Copying and distribution of this file, with or without modification, are permitted in any medium without royalty provided the copyright notice and -- 2.39.2