1 /* Copyright (c) 2012, 2016, Oracle and/or its affiliates. All rights reserved.
2
3 This program is free software; you can redistribute it and/or modify
4 it under the terms of the GNU General Public License, version 2.0,
5 as published by the Free Software Foundation.
6
7 This program is also distributed with certain software (including
8 but not limited to OpenSSL) that is licensed under separate terms,
9 as designated in a particular file or component or in included license
10 documentation. The authors of MySQL hereby grant you an additional
11 permission to link the program and your derivative works with the
12 separately licensed software that they have included with MySQL.
13
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License, version 2.0, for more details.
18
19 You should have received a copy of the GNU General Public License
20 along with this program; if not, write to the Free Software
21 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */
22
23 #include <my_sys.h>
24 #include <string>
25 #include <mysql/plugin_validate_password.h>
26 #include <mysql/service_my_plugin_log.h>
27 #include <mysql/service_mysql_string.h>
28 #include <set>
29 #include <iostream>
30 #include <fstream>
31 #include <algorithm> // std::swap
32 THD *thd_get_current_thd(); // from sql_class.cc
33
34
35
36 #define MAX_DICTIONARY_FILE_LENGTH 1024 * 1024
37 #define PASSWORD_SCORE 25
38 #define MIN_DICTIONARY_WORD_LENGTH 4
39 #define MAX_PASSWORD_LENGTH 100
40
41 /* Read-write lock for dictionary_words cache */
42 mysql_rwlock_t LOCK_dict_file;
43
44 #ifdef HAVE_PSI_INTERFACE
45 PSI_rwlock_key key_validate_password_LOCK_dict_file;
46
47 static PSI_rwlock_info all_validate_password_rwlocks[]=
48 {
49 { &key_validate_password_LOCK_dict_file, "LOCK_dict_file", 0}
50 };
51
init_validate_password_psi_keys()52 void init_validate_password_psi_keys()
53 {
54 const char* category= "validate";
55 int count;
56
57 count= array_elements(all_validate_password_rwlocks);
58 mysql_rwlock_register(category, all_validate_password_rwlocks, count);
59 }
60 #endif /* HAVE_PSI_INTERFACE */
61
62
63 /*
64 Handle assigned when loading the plugin.
65 Used with the error reporting functions.
66 */
67
68 static MYSQL_PLUGIN plugin_info_ptr;
69 /*
70 These are the 3 password policies that this plugin allow to set
71 and configure as per the requirements.
72 */
73
74 enum password_policy_enum { PASSWORD_POLICY_LOW,
75 PASSWORD_POLICY_MEDIUM,
76 PASSWORD_POLICY_STRONG
77 };
78
79 static const char* policy_names[] = { "LOW", "MEDIUM", "STRONG", NullS };
80
81 static TYPELIB password_policy_typelib_t = {
82 array_elements(policy_names) - 1,
83 "password_policy_typelib_t",
84 policy_names,
85 NULL
86 };
87
88 typedef std::string string_type;
89 typedef std::set<string_type> set_type;
90 static set_type dictionary_words;
91
92 static int validate_password_length;
93 static int validate_password_number_count;
94 static int validate_password_mixed_case_count;
95 static int validate_password_special_char_count;
96 static ulong validate_password_policy;
97 static char *validate_password_dictionary_file;
98 static char *validate_password_dictionary_file_last_parsed= NULL;
99 static long long validate_password_dictionary_file_words_count= 0;
100 static my_bool check_user_name;
101
102 /**
103 Activate the new dictionary
104
105 Assigns a local list to the global variable,
106 taking the correct locks in the process.
107 Also updates the status variables.
108 @param dict_words new dictionary words set
109
110 */
dictionary_activate(set_type * dict_words)111 static void dictionary_activate(set_type *dict_words)
112 {
113 time_t start_time;
114 struct tm tm;
115 char timebuf[20]; /* "YYYY-MM-DD HH:MM:SS" */
116 char *new_ts;
117
118 /* fetch the start time */
119 start_time= my_time(MYF(0));
120 localtime_r(&start_time, &tm);
121 my_snprintf(timebuf, sizeof(timebuf), "%04d-%02d-%02d %02d:%02d:%02d",
122 tm.tm_year + 1900,
123 tm.tm_mon + 1,
124 tm.tm_mday,
125 tm.tm_hour,
126 tm.tm_min,
127 tm.tm_sec);
128 new_ts= my_strdup(PSI_NOT_INSTRUMENTED, timebuf, MYF(0));
129
130 mysql_rwlock_wrlock(&LOCK_dict_file);
131 std::swap(dictionary_words, *dict_words);
132 validate_password_dictionary_file_words_count= dictionary_words.size();
133 std::swap(new_ts, validate_password_dictionary_file_last_parsed);
134 mysql_rwlock_unlock(&LOCK_dict_file);
135
136 /* frees up the data just replaced */
137 if (!dict_words->empty())
138 dict_words->clear();
139 if (new_ts)
140 my_free(new_ts);
141 }
142
143
144 /* To read dictionary file into std::set */
read_dictionary_file()145 static void read_dictionary_file()
146 {
147 string_type words;
148 set_type dict_words;
149 std::streamoff file_length;
150
151 if (validate_password_dictionary_file == NULL)
152 {
153 if (validate_password_policy == PASSWORD_POLICY_STRONG)
154 my_plugin_log_message(&plugin_info_ptr, MY_WARNING_LEVEL,
155 "Dictionary file not specified");
156 /* NULL is a valid value, despite the warning */
157 dictionary_activate(&dict_words);
158 return;
159 }
160 try
161 {
162 std::ifstream dictionary_stream(validate_password_dictionary_file);
163 if (!dictionary_stream || !dictionary_stream.is_open())
164 {
165 my_plugin_log_message(&plugin_info_ptr, MY_WARNING_LEVEL,
166 "Dictionary file not loaded");
167 return;
168 }
169 dictionary_stream.seekg(0, std::ios::end);
170 file_length= dictionary_stream.tellg();
171 dictionary_stream.seekg(0, std::ios::beg);
172 if (file_length > MAX_DICTIONARY_FILE_LENGTH)
173 {
174 dictionary_stream.close();
175 my_plugin_log_message(&plugin_info_ptr, MY_WARNING_LEVEL,
176 "Dictionary file size exceeded",
177 "MAX_DICTIONARY_FILE_LENGTH, not loaded");
178 return;
179 }
180 for (std::getline(dictionary_stream, words); dictionary_stream.good();
181 std::getline(dictionary_stream, words))
182 dict_words.insert(words);
183 dictionary_stream.close();
184 dictionary_activate(&dict_words);
185 }
186 catch (...) // no exceptions !
187 {
188 my_plugin_log_message(&plugin_info_ptr, MY_WARNING_LEVEL,
189 "Exception while reading the dictionary file");
190 }
191 }
192
193
194 /* Clear words from std::set */
free_dictionary_file()195 static void free_dictionary_file()
196 {
197 mysql_rwlock_wrlock(&LOCK_dict_file);
198 if (!dictionary_words.empty())
199 dictionary_words.clear();
200 if (validate_password_dictionary_file_last_parsed)
201 {
202 my_free(validate_password_dictionary_file_last_parsed);
203 validate_password_dictionary_file_last_parsed= NULL;
204 }
205 mysql_rwlock_unlock(&LOCK_dict_file);
206 }
207
208 /*
209 Checks whether password or substring of password
210 is present in dictionary file stored as std::set
211 */
validate_dictionary_check(mysql_string_handle password)212 static int validate_dictionary_check(mysql_string_handle password)
213 {
214 int length;
215 int error= 0;
216 char *buffer;
217
218 if (dictionary_words.empty())
219 return (1);
220
221 /* New String is allocated */
222 mysql_string_handle lower_string_handle= mysql_string_to_lowercase(password);
223 if (!(buffer= (char*) malloc(MAX_PASSWORD_LENGTH)))
224 return (0);
225
226 length= mysql_string_convert_to_char_ptr(lower_string_handle, "utf8",
227 buffer, MAX_PASSWORD_LENGTH,
228 &error);
229 /* Free the allocated string */
230 mysql_string_free(lower_string_handle);
231 int substr_pos= 0;
232 int substr_length= length;
233 string_type password_str= string_type((const char *)buffer, length);
234 string_type password_substr;
235 set_type::iterator itr;
236 /*
237 std::set as container stores the dictionary words,
238 binary comparison between dictionary words and password
239 */
240 mysql_rwlock_rdlock(&LOCK_dict_file);
241 while (substr_length >= MIN_DICTIONARY_WORD_LENGTH)
242 {
243 substr_pos= 0;
244 while (substr_pos + substr_length <= length)
245 {
246 password_substr= password_str.substr(substr_pos, substr_length);
247 itr= dictionary_words.find(password_substr);
248 if (itr != dictionary_words.end())
249 {
250 mysql_rwlock_unlock(&LOCK_dict_file);
251 free(buffer);
252 return (0);
253 }
254 substr_pos++;
255 }
256 substr_length--;
257 }
258 mysql_rwlock_unlock(&LOCK_dict_file);
259 free(buffer);
260 return (1);
261 }
262
263
264 /**
265 Compare a sequence of bytes in "a" with the reverse sequence of bytes of "b"
266
267 @param a the first sequence
268 @param a_len the length of a
269 @param b the second sequence
270 @param b_len the length of b
271
272 @retval true sequences match
273 @retval false sequences don't match
274 */
my_memcmp_reverse(const char * a,size_t a_len,const char * b,size_t b_len)275 static bool my_memcmp_reverse(const char *a, size_t a_len,
276 const char *b, size_t b_len)
277 {
278 const char *a_ptr;
279 const char *b_ptr;
280
281 if (a_len != b_len)
282 return false;
283
284 for (a_ptr= a, b_ptr= b + b_len - 1; b_ptr >= b; a_ptr++, b_ptr--)
285 if (*a_ptr != *b_ptr)
286 return false;
287 return true;
288 }
289
290 /**
291 Validate a user name from the security context
292
293 A helper function.
294 Validates one user name (as specified by field_name)
295 against the data in buffer/length by comparing the byte
296 sequences in forward and reverse.
297
298 Logs an error to the error log if it can't pick up the user names.
299
300 @param ctx the current security context
301 @param buffer the password data
302 @param length the length of buffer
303 @param field_name the id of the security context field to use
304 @param logical_name the name of the field to use in the error message
305
306 @retval true name can be used
307 @retval false name is invalid
308 */
is_valid_user(MYSQL_SECURITY_CONTEXT ctx,const char * buffer,int length,const char * field_name,const char * logical_name)309 static bool is_valid_user(MYSQL_SECURITY_CONTEXT ctx,
310 const char *buffer, int length,
311 const char *field_name,
312 const char *logical_name)
313 {
314 MYSQL_LEX_CSTRING user={ NULL, 0 };
315
316 if (security_context_get_option(ctx, field_name, &user))
317 {
318 my_plugin_log_message(&plugin_info_ptr, MY_ERROR_LEVEL,
319 "Can't retrieve the %s from the"
320 "security context", logical_name);
321 return false;
322 }
323
324 /* lengths must match for the strings to match */
325 if (user.length != (size_t) length)
326 return true;
327 /* empty strings turn the check off */
328 if (user.length == 0)
329 return true;
330 /* empty strings turn the check off */
331 if (!user.str)
332 return true;
333
334 return (0 != memcmp(buffer, user.str, user.length) &&
335 !my_memcmp_reverse(user.str, user.length, buffer, length));
336 }
337
338
339 /**
340 Check if the password is not the user name
341
342 Helper function.
343 Checks if the password supplied is valid to use by comparing it
344 the effected and the login user names to it and to the reverse of it.
345 logs an error to the error log if it can't pick up the names.
346
347 @param password the password handle
348 @retval true The password can be used
349 @retval false the password is invalid
350 */
is_valid_password_by_user_name(mysql_string_handle password)351 static bool is_valid_password_by_user_name(mysql_string_handle password)
352 {
353 char buffer[MAX_PASSWORD_LENGTH];
354 int length, error;
355 MYSQL_SECURITY_CONTEXT ctx= NULL;
356
357 if (!check_user_name)
358 return true;
359
360 if (thd_get_security_context(thd_get_current_thd(), &ctx) || !ctx)
361 {
362 my_plugin_log_message(&plugin_info_ptr, MY_ERROR_LEVEL,
363 "Can't retrieve the security context");
364 return false;
365 }
366
367 length= mysql_string_convert_to_char_ptr(password, "utf8",
368 buffer, MAX_PASSWORD_LENGTH,
369 &error);
370
371 return
372 is_valid_user(ctx, buffer, length, "user", "login user name")
373 && is_valid_user(ctx, buffer, length, "priv_user", "effective user name");
374 }
375
validate_password_policy_strength(mysql_string_handle password,int policy)376 static int validate_password_policy_strength(mysql_string_handle password,
377 int policy)
378 {
379 int has_digit= 0;
380 int has_lower= 0;
381 int has_upper= 0;
382 int has_special_chars= 0;
383 int n_chars= 0;
384 mysql_string_iterator_handle iter;
385
386 iter = mysql_string_get_iterator(password);
387 while(mysql_string_iterator_next(iter))
388 {
389 n_chars++;
390 if (policy > PASSWORD_POLICY_LOW)
391 {
392 if (mysql_string_iterator_islower(iter))
393 has_lower++;
394 else if (mysql_string_iterator_isupper(iter))
395 has_upper++;
396 else if (mysql_string_iterator_isdigit(iter))
397 has_digit++;
398 else
399 has_special_chars++;
400 }
401 }
402
403 mysql_string_iterator_free(iter);
404 if (n_chars >= validate_password_length)
405 {
406 if (!is_valid_password_by_user_name(password))
407 return(0);
408
409 if (policy == PASSWORD_POLICY_LOW)
410 return (1);
411 if (has_upper >= validate_password_mixed_case_count &&
412 has_lower >= validate_password_mixed_case_count &&
413 has_special_chars >= validate_password_special_char_count &&
414 has_digit >= validate_password_number_count)
415 {
416 if (policy == PASSWORD_POLICY_MEDIUM || validate_dictionary_check
417 (password))
418 return (1);
419 }
420 }
421 return (0);
422 }
423
424 /* Actual plugin function which acts as a wrapper */
validate_password(mysql_string_handle password)425 static int validate_password(mysql_string_handle password)
426 {
427 return validate_password_policy_strength(password, validate_password_policy);
428 }
429
430 /* Password strength between (0-100) */
get_password_strength(mysql_string_handle password)431 static int get_password_strength(mysql_string_handle password)
432 {
433 int policy= 0;
434 int n_chars= 0;
435 mysql_string_iterator_handle iter;
436
437 if (!is_valid_password_by_user_name(password))
438 return 0;
439
440 iter = mysql_string_get_iterator(password);
441 while(mysql_string_iterator_next(iter))
442 n_chars++;
443
444 mysql_string_iterator_free(iter);
445 if (n_chars < MIN_DICTIONARY_WORD_LENGTH)
446 return (policy);
447 if (n_chars < validate_password_length)
448 return (PASSWORD_SCORE);
449 else
450 {
451 policy= PASSWORD_POLICY_LOW;
452 if (validate_password_policy_strength(password, PASSWORD_POLICY_MEDIUM))
453 {
454 policy= PASSWORD_POLICY_MEDIUM;
455 if (validate_dictionary_check(password))
456 policy= PASSWORD_POLICY_STRONG;
457 }
458 }
459 return ((policy+1) * PASSWORD_SCORE + PASSWORD_SCORE);
460 }
461
462 /**
463 @brief Check and readjust effective value of validate_password_length
464
465 @details
466 Readjust validate_password_length according to the values of
467 validate_password_number_count,validate_password_mixed_case_count
468 and validate_password_special_char_count. This is required at the
469 time plugin installation and as a part of setting new values for
470 any of above mentioned variables.
471
472 */
473 static void
readjust_validate_password_length()474 readjust_validate_password_length()
475 {
476 int policy_password_length;
477
478 /*
479 Effective value of validate_password_length variable is:
480
481 MAX(validate_password_length,
482 (validate_password_number_count +
483 2*validate_password_mixed_case_count +
484 validate_password_special_char_count))
485 */
486 policy_password_length= (validate_password_number_count +
487 (2 * validate_password_mixed_case_count) +
488 validate_password_special_char_count);
489
490 if (validate_password_length < policy_password_length)
491 {
492 /*
493 Raise a warning that effective restriction on password
494 length is changed.
495 */
496 my_plugin_log_message(&plugin_info_ptr, MY_WARNING_LEVEL,
497 "Effective value of validate_password_length is changed."
498 " New value is %d",
499 policy_password_length);
500
501 validate_password_length= policy_password_length;
502 }
503 }
504
505 /* Plugin type-specific descriptor */
506 static struct st_mysql_validate_password validate_password_descriptor=
507 {
508 MYSQL_VALIDATE_PASSWORD_INTERFACE_VERSION,
509 validate_password, /* validate function */
510 get_password_strength /* validate strength function */
511 };
512
513 /*
514 Initialize the password plugin at server start or plugin installation,
515 read dictionary file into std::set.
516 */
517
validate_password_init(MYSQL_PLUGIN plugin_info)518 static int validate_password_init(MYSQL_PLUGIN plugin_info)
519 {
520 plugin_info_ptr= plugin_info;
521 #ifdef HAVE_PSI_INTERFACE
522 init_validate_password_psi_keys();
523 #endif
524 mysql_rwlock_init(key_validate_password_LOCK_dict_file, &LOCK_dict_file);
525 read_dictionary_file();
526 /* Check if validate_password_length needs readjustment */
527 readjust_validate_password_length();
528 return (0);
529 }
530
531 /*
532 Terminate the password plugin at server shutdown or plugin deinstallation.
533 It empty the std::set and returns 0
534 */
535
validate_password_deinit(void * arg MY_ATTRIBUTE ((unused)))536 static int validate_password_deinit(void *arg MY_ATTRIBUTE((unused)))
537 {
538 free_dictionary_file();
539 mysql_rwlock_destroy(&LOCK_dict_file);
540 return (0);
541 }
542
543 /*
544 Update function for validate_password_dictionary_file.
545 If dictionary file is changed, this function will flush
546 the cache and re-load the new dictionary file.
547 */
548 static void
dictionary_update(MYSQL_THD thd MY_ATTRIBUTE ((unused)),struct st_mysql_sys_var * var MY_ATTRIBUTE ((unused)),void * var_ptr,const void * save)549 dictionary_update(MYSQL_THD thd MY_ATTRIBUTE((unused)),
550 struct st_mysql_sys_var *var MY_ATTRIBUTE((unused)),
551 void *var_ptr, const void *save)
552 {
553 *(const char**)var_ptr= *(const char**)save;
554 read_dictionary_file();
555 }
556
557 /*
558 update function for:
559 1. validate_password_length
560 2. validate_password_number_count
561 3. validate_password_mixed_case_count
562 4. validate_password_special_char_count
563 */
564 static void
length_update(MYSQL_THD thd MY_ATTRIBUTE ((unused)),struct st_mysql_sys_var * var MY_ATTRIBUTE ((unused)),void * var_ptr,const void * save)565 length_update(MYSQL_THD thd MY_ATTRIBUTE((unused)),
566 struct st_mysql_sys_var *var MY_ATTRIBUTE((unused)),
567 void *var_ptr, const void *save)
568 {
569 /* check if there is an actual change */
570 if (*((int *)var_ptr) == *((int *)save))
571 return;
572
573 /*
574 set new value for system variable.
575 Note that we need not know for which of the above mentioned
576 variables, length_update() is called because var_ptr points
577 to the location at which corresponding static variable is
578 declared in this file.
579 */
580 *((int *)var_ptr)= *((int *)save);
581
582 readjust_validate_password_length();
583 }
584
585
586
587 /* Plugin system variables */
588
589 static MYSQL_SYSVAR_INT(length, validate_password_length,
590 PLUGIN_VAR_RQCMDARG,
591 "Password validate length to check for minimum password_length",
592 NULL, length_update, 8, 0, 0, 0);
593
594 static MYSQL_SYSVAR_INT(number_count, validate_password_number_count,
595 PLUGIN_VAR_RQCMDARG,
596 "password validate digit to ensure minimum numeric character in password",
597 NULL, length_update, 1, 0, 0, 0);
598
599 static MYSQL_SYSVAR_INT(mixed_case_count, validate_password_mixed_case_count,
600 PLUGIN_VAR_RQCMDARG,
601 "Password validate mixed case to ensure minimum upper/lower case in password",
602 NULL, length_update, 1, 0, 0, 0);
603
604 static MYSQL_SYSVAR_INT(special_char_count,
605 validate_password_special_char_count, PLUGIN_VAR_RQCMDARG,
606 "password validate special to ensure minimum special character in password",
607 NULL, length_update, 1, 0, 0, 0);
608
609 static MYSQL_SYSVAR_ENUM(policy, validate_password_policy,
610 PLUGIN_VAR_RQCMDARG,
611 "password_validate_policy choosen policy to validate password"
612 "possible values are LOW MEDIUM (default), STRONG",
613 NULL, NULL, PASSWORD_POLICY_MEDIUM, &password_policy_typelib_t);
614
615 static MYSQL_SYSVAR_STR(dictionary_file, validate_password_dictionary_file,
616 PLUGIN_VAR_RQCMDARG | PLUGIN_VAR_MEMALLOC,
617 "password_validate_dictionary file to be loaded and check for password",
618 NULL, dictionary_update, NULL);
619
620 static MYSQL_SYSVAR_BOOL(check_user_name, check_user_name,
621 PLUGIN_VAR_NOCMDARG,
622 "Check if the password matches the login or the effective user names "
623 "or the reverse of them",
624 NULL, NULL, FALSE);
625
626 static struct st_mysql_sys_var* validate_password_system_variables[]= {
627 MYSQL_SYSVAR(length),
628 MYSQL_SYSVAR(number_count),
629 MYSQL_SYSVAR(mixed_case_count),
630 MYSQL_SYSVAR(special_char_count),
631 MYSQL_SYSVAR(policy),
632 MYSQL_SYSVAR(dictionary_file),
633 MYSQL_SYSVAR(check_user_name),
634 NULL
635 };
636
637 static struct st_mysql_show_var validate_password_status_variables[]= {
638 { "validate_password_dictionary_file_last_parsed",
639 (char *) &validate_password_dictionary_file_last_parsed,
640 SHOW_CHAR_PTR, SHOW_SCOPE_GLOBAL },
641 { "validate_password_dictionary_file_words_count",
642 (char *) &validate_password_dictionary_file_words_count,
643 SHOW_LONGLONG, SHOW_SCOPE_GLOBAL },
644 { NullS, NullS, SHOW_LONG, SHOW_SCOPE_GLOBAL }
645 };
646
mysql_declare_plugin(validate_password)647 mysql_declare_plugin(validate_password)
648 {
649 MYSQL_VALIDATE_PASSWORD_PLUGIN, /* type */
650 &validate_password_descriptor, /* descriptor */
651 "validate_password", /* name */
652 "Oracle Corporation", /* author */
653 "check password strength", /* description */
654 PLUGIN_LICENSE_GPL,
655 validate_password_init, /* init function (when loaded) */
656 validate_password_deinit, /* deinit function (when unloaded) */
657 0x0101, /* version */
658 validate_password_status_variables, /* status variables */
659 validate_password_system_variables, /* system variables */
660 NULL,
661 0,
662 }
663 mysql_declare_plugin_end;
664