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