]> eyrie.org Git - kerberos/krb5-strength.git/commitdiff
Add support for SQLite dictionaries
authorRuss Allbery <eagle@eyrie.org>
Tue, 25 Mar 2014 07:09:39 +0000 (00:09 -0700)
committerRuss Allbery <eagle@eyrie.org>
Tue, 25 Mar 2014 07:09:39 +0000 (00:09 -0700)
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.

13 files changed:
Makefile.am
NEWS
README
configure.ac
plugin/general.c
plugin/internal.h
plugin/sqlite.c [new file with mode: 0644]
tests/data/passwords/sqlite.json [new file with mode: 0644]
tests/data/wordlist
tests/plugin/heimdal-t.c
tests/plugin/mit-t.c
tests/tools/heimdal-strength-t
tools/heimdal-strength.pod

index 7fce813f27e3739b85287d3e72ce8647aa477047..c2286f9583e2dda3ab8ef493b28299d73c7fc1fe 100644 (file)
@@ -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 96758beb13ed66b35b8dee160e9acdddf402cb65..20726e5f202b14fc84ec5e24707636ce9c0c5d9d 100644 (file)
--- 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 3ad8765a6efa43d30ab2bf05353168536a9b814d..dbe519ecb7f11693a8284cf188ed470b9615f66e 100644 (file)
--- 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
 
index be9468d17b8f342c58b431b3cb8250019f2b9386..89bcc9e7cd4e7a8bde1a8f54f04655a3c6e27ed0 100644 (file)
@@ -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"])
index bd8d56f13779d099df340d3fe01c7e9e0e4cdc5f..c491ea01b9522742e2fd2afc0ff4f5cb587a4ccd 100644 (file)
@@ -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;
index 545f7bee7efde7d3a0ba4ce2035a46eeb326699c..72b5a12a19565d6d171d0e5dd2161790b9f8959a 100644 (file)
@@ -20,6 +20,9 @@
 #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
@@ -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 (file)
index 0000000..56f8d2c
--- /dev/null
@@ -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 <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 */
diff --git a/tests/data/passwords/sqlite.json b/tests/data/passwords/sqlite.json
new file mode 100644 (file)
index 0000000..f736e02
--- /dev/null
@@ -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
+    }
+]
index 3a33db85b4c026cdc84ba3b2b5ae27682ee1b7d9..bc0e16e6fac0ced21eb5e566148b1bc3411af40f 100644 (file)
@@ -1,3 +1,4 @@
+ab
 bitterbane
 happenstance
 one
index b32c944fbebaf8c94841b0e4cbea438d5fb5d03f..6fc561870265e881d23f0c468a86b5b7976b948a 100644 (file)
@@ -25,8 +25,8 @@
 #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>
@@ -34,6 +34,7 @@
 #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
@@ -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);
index 7026f441a8451ae5cc2c290a9eac3638202de24d..09401c3101a163109d7477056d97f7b1c7bc44bc 100644 (file)
@@ -26,8 +26,8 @@
 #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>
@@ -35,6 +35,7 @@
 #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
@@ -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);
index ca258155c7616a46016dc411a400ddd2055ef3c7..fd83ab185448d83b60617b23e883495abd081b05 100755 (executable)
@@ -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;
index 9e26a09bcf547da345d12719c62fb7ee7ddc1b0b..422f3a186ec7a1fa6ea18027800bdfcfb5e34db2 100644 (file)
@@ -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<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
@@ -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<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
 
@@ -163,8 +184,8 @@ Russ Allbery <eagle@eyrie.org>
 
 =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