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
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
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
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
[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
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
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"])
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;
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;
if (data == NULL)
return;
strength_close_cdb(ctx, data);
+ strength_close_sqlite(ctx, data);
last = data->rules;
while (last != NULL) {
tmp = last;
#ifdef HAVE_CDB_H
# include <cdb.h>
#endif
+#ifdef HAVE_SQLITE3_H
+# include <sqlite3.h>
+#endif
#include <stddef.h>
#ifdef HAVE_KRB5_PWQUAL_PLUGIN_H
#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
*
* 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
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);
--- /dev/null
+/*
+ * 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 <eagle@eyrie.org>
+ * 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 <config.h>
+#include <portable/kadmin.h>
+#include <portable/krb5.h>
+#include <portable/system.h>
+
+#ifdef HAVE_SQLITE3_H
+# include <sqlite3.h>
+#endif
+
+#include <plugin/internal.h>
+#include <util/macros.h>
+
+/*
+ * 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 */
--- /dev/null
+[
+ {
+ "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
+ }
+]
+ab
bitterbane
happenstance
one
#include <util/macros.h>
/*
- * 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 <tests/data/passwords/cdb.c>
#include <tests/data/passwords/classes.c>
#include <tests/data/passwords/length.c>
#include <tests/data/passwords/letter.c>
#include <tests/data/passwords/principal.c>
+#include <tests/data/passwords/sqlite.c>
#ifndef HAVE_KADM5_KADM5_PWCHECK_H
/*
* 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. */
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++)
#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);
#include <util/macros.h>
/*
- * 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 <tests/data/passwords/cdb.c>
#include <tests/data/passwords/classes.c>
#include <tests/data/passwords/length.c>
#include <tests/data/passwords/letter.c>
#include <tests/data/passwords/principal.c>
+#include <tests/data/passwords/sqlite.c>
#ifndef HAVE_KRB5_PWQUAL_PLUGIN_H
/*
* 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");
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);
#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);
# 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.
}
}
+# 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;
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<cdbmake-wordlist> utility to generate the CDB database
-from a word list.
+You can use the B<krb5-strength-wordlist> 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
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<passwords>. This table should have two columns, C<password>
+and C<drowssap>, which, for each dictionary word, holds the word and the
+reversed form of the word. You can use the B<krb5-strength-wordlist>
+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
=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