1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398
|
/*
* 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_SQLITE
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_SQLITE
/*
* Report a SQLite error. Takes the module data (used to access the SQLite
* object) and the Kerberos context, stores the SQLite 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, krb5_pwqual_moddata data, const char *format,
...)
{
va_list args;
ssize_t length;
char *message;
const char *errmsg;
errmsg = sqlite3_errmsg(data->sqlite);
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, errmsg);
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 password 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 greater than or
* equal to the length of the password minus one.
*
* To see why the sum of the prefix and suffix length can be longer than the
* length of the password when the password doesn't match the word, consider
* the password "aaaa" and the word "aaaaaaaaa"
* (The prefix length plus the
* suffix length may be greater than the length of the password if the
* password is an exact match for the word or
*/
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, match_length, word_length;
/* Discard all words whose length is too different. */
word = (const char *) sqlite3_column_text(query, 0);
word_length = strlen(word);
if (length > word_length + 1 || length + 1 < word_length)
return false;
/*
* Get the common prefix length and check if the password is an exact
* match.
*/
prefix_length = common_prefix_length(password, word);
if (prefix_length == length)
return true;
/*
* Ensure there aren't too many different characters for this to be a
* match. If the common prefix and the common suffix together have a
* length that's more than one character shorter than the password length,
* this is different by at least edit distance two. The sum of the
* lengths of the common prefix and suffix can be greater than length in
* cases of an edit in the middle of repeated passwords, such as the
* password "baaab" and the word "baab", but those are all matches.
*/
drow = (const char *) sqlite3_column_text(query, 1);
suffix_length = common_prefix_length(drowssap, drow);
match_length = prefix_length + suffix_length;
return (match_length > length || length - match_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, data, "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, data, "cannot prepare prefix query");
status = sqlite3_prepare_v2(data->sqlite, SUFFIX_QUERY, -1,
&data->suffix_query, NULL);
if (status != 0)
return error_sqlite(ctx, data, "cannot prepare suffix query");
/* Finished. Return success. */
free(path);
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, data, "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, data, "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, data, "error searching by password prefix");
goto fail;
}
status = sqlite3_reset(data->prefix_query);
if (status != SQLITE_OK) {
code = error_sqlite(ctx, data, "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, data, "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, data, "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, data, "error searching by password suffix");
goto fail;
}
status = sqlite3_reset(data->suffix_query);
if (status != SQLITE_OK) {
code = error_sqlite(ctx, data, "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(data->sqlite);
}
#endif /* HAVE_SQLITE */
|