@@ -30,11 +30,13 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */
3030#include < iomanip>
3131#include < set> // std::set
3232#include < sstream>
33+ #include < unordered_set>
3334
3435#include " mysql/components/library_mysys/my_memory.h"
3536#include " mysql/components/services/mysql_rwlock.h"
3637#include " mysql/components/services/psi_memory.h"
3738#include " mysqld_error.h"
39+ #include " scope_guard.h"
3840
3941#define PSI_NOT_INSTRUMENTED 0
4042const int MAX_DICTIONARY_FILE_LENGTH = (1024 * 1024 );
@@ -90,6 +92,7 @@ static char *validate_password_dictionary_file;
9092static char *validate_password_dictionary_file_last_parsed = nullptr ;
9193static long long validate_password_dictionary_file_words_count = 0 ;
9294static bool check_user_name;
95+ static int validate_password_changed_characters_percentage = 0 ;
9396/*
9497 This variable is used, to make sure the use of component services
9598 after the component load/initialization is done.
@@ -594,6 +597,114 @@ DEFINE_BOOL_METHOD(validate_password_imp::validate,
594597 validate_password_policy) == 0 );
595598}
596599
600+ /* *
601+ Validate if number of changed characters matches the pre-configured
602+ criteria
603+
604+ @param [in] current_password Current password
605+ @param [in] new_password New password
606+ @param [out] minimum_required Minimum required number of changed characters
607+ @param [out] changed Actual number of changed characters
608+
609+ @returns Result of validation
610+ @retval false Success
611+ @retval true Error
612+ */
613+ DEFINE_BOOL_METHOD (validate_password_changed_characters_imp::validate,
614+ (my_h_string current_password, my_h_string new_password,
615+ uint *minimum_required, uint *changed)) {
616+ try {
617+ uint current_length = 0 , new_length = 0 ;
618+ if (changed) *changed = 0 ;
619+
620+ /* quick exit if restriction is not imposed */
621+ if (validate_password_changed_characters_percentage == 0 ) return false ;
622+
623+ /* Convert passwords to lowercase before comparison */
624+ my_h_string current_password_lc, new_password_lc;
625+ if (mysql_service_mysql_string_factory->create (¤t_password_lc) ||
626+ mysql_service_mysql_string_factory->create (&new_password_lc)) {
627+ LogEvent ()
628+ .type (LOG_TYPE_ERROR)
629+ .prio (ERROR_LEVEL)
630+ .lookup (ER_VALIDATE_PWD_STRING_HANDLER_MEM_ALLOCATION_FAILED);
631+ return true ;
632+ }
633+
634+ auto cleanup_guard = create_scope_guard ([&] {
635+ mysql_service_mysql_string_factory->destroy (current_password_lc);
636+ mysql_service_mysql_string_factory->destroy (new_password_lc);
637+ });
638+
639+ if (mysql_service_mysql_string_case->tolower (¤t_password_lc,
640+ current_password) ||
641+ mysql_service_mysql_string_case->tolower (&new_password_lc,
642+ new_password)) {
643+ LogEvent ()
644+ .type (LOG_TYPE_ERROR)
645+ .prio (ERROR_LEVEL)
646+ .lookup (ER_VALIDATE_PWD_STRING_CONV_TO_LOWERCASE_FAILED);
647+ return true ;
648+ }
649+
650+ if (mysql_service_mysql_string_character_access->get_char_length (
651+ current_password_lc, ¤t_length) ||
652+ mysql_service_mysql_string_character_access->get_char_length (
653+ new_password_lc, &new_length)) {
654+ return true ;
655+ }
656+
657+ /* Determine number of characters required to be changed */
658+ uint number_of_characters_to_be_changed =
659+ (std::max (static_cast <uint>(validate_password_length), current_length) *
660+ (static_cast <uint>(validate_password_changed_characters_percentage)) /
661+ 100 );
662+
663+ if (minimum_required)
664+ *minimum_required = number_of_characters_to_be_changed;
665+
666+ std::unordered_set<long > characters;
667+ auto process_password = [&characters](my_h_string password,
668+ bool add) -> bool {
669+ int pos = 0 ;
670+ ulong character = 0 ;
671+ my_h_string_iterator password_iterator{nullptr };
672+
673+ if (mysql_service_mysql_string_iterator->iterator_create (
674+ password, &password_iterator))
675+ return true ;
676+
677+ auto iterator_cleanup = create_scope_guard ([&] {
678+ mysql_service_mysql_string_iterator->iterator_destroy (
679+ password_iterator);
680+ });
681+
682+ while (!mysql_service_mysql_string_iterator->iterator_get_next (
683+ password_iterator, &pos)) {
684+ if (mysql_service_mysql_string_value->get (password_iterator,
685+ &character))
686+ return true ;
687+
688+ if (add)
689+ (void )characters.insert (character);
690+ else
691+ (void )characters.erase (character);
692+ }
693+ return false ;
694+ };
695+
696+ if (process_password (new_password_lc, true )) return true ;
697+
698+ if (process_password (current_password_lc, false )) return true ;
699+
700+ if (changed) *changed = characters.size ();
701+
702+ return (characters.size () < number_of_characters_to_be_changed);
703+ } catch (...) {
704+ return true ;
705+ }
706+ }
707+
597708int register_status_variables () {
598709 if (mysql_service_status_variable_registration->register_variable (
599710 (SHOW_VAR *)&validate_password_status_variables)) {
@@ -607,7 +718,10 @@ int register_status_variables() {
607718}
608719
609720int register_system_variables () {
610- INTEGRAL_CHECK_ARG (int ) length, num_count, mixed_case_count, spl_char_count;
721+ INTEGRAL_CHECK_ARG (int )
722+ length, num_count, mixed_case_count, spl_char_count,
723+ changed_characters_percentage;
724+
611725 length.def_val = 8 ;
612726 length.min_val = 0 ;
613727 length.max_val = 0 ;
@@ -731,7 +845,30 @@ int register_system_variables() {
731845 " validate_password.check_user_name" );
732846 goto check_user_name;
733847 }
848+
849+ changed_characters_percentage.def_val = 0 ;
850+ changed_characters_percentage.min_val = 0 ;
851+ changed_characters_percentage.max_val = 100 ;
852+ changed_characters_percentage.blk_sz = 0 ;
853+ if (mysql_service_component_sys_variable_register->register_variable (
854+ " validate_password" , " changed_characters_percentage" ,
855+ PLUGIN_VAR_INT | PLUGIN_VAR_RQCMDARG,
856+ " password validate percentage of changed characters required in new "
857+ " password. Valid values between 0 and 100." ,
858+ nullptr , length_update, (void *)&changed_characters_percentage,
859+ (void *)&validate_password_changed_characters_percentage)) {
860+ LogEvent ()
861+ .type (LOG_TYPE_ERROR)
862+ .prio (ERROR_LEVEL)
863+ .lookup (ER_VALIDATE_PWD_VARIABLE_REGISTRATION_FAILED,
864+ " validate_password.changed_characters_percentage" );
865+ goto changed_characters_percentage;
866+ }
734867 return 0 ; /* All system variables registered successfully */
868+
869+ changed_characters_percentage:
870+ mysql_service_component_sys_variable_unregister->unregister_variable (
871+ " validate_password" , " check_user_name" );
735872check_user_name:
736873 mysql_service_component_sys_variable_unregister->unregister_variable (
737874 " validate_password" , " dictionary_file" );
@@ -828,6 +965,15 @@ int unregister_system_variables() {
828965 .lookup (ER_VALIDATE_PWD_VARIABLE_UNREGISTRATION_FAILED,
829966 " validate_password.check_user_name" );
830967 }
968+
969+ if (mysql_service_component_sys_variable_unregister->unregister_variable (
970+ " validate_password" , " changed_characters_percentage" )) {
971+ LogEvent ()
972+ .type (LOG_TYPE_ERROR)
973+ .prio (ERROR_LEVEL)
974+ .lookup (ER_VALIDATE_PWD_VARIABLE_UNREGISTRATION_FAILED,
975+ " validate_password.changed_characters_percentage" );
976+ }
831977 return 0 ;
832978}
833979
@@ -921,21 +1067,28 @@ BEGIN_SERVICE_IMPLEMENTATION(validate_password, validate_password)
9211067validate_password_imp::validate,
9221068 validate_password_imp::get_strength END_SERVICE_IMPLEMENTATION();
9231069
1070+ BEGIN_SERVICE_IMPLEMENTATION (validate_password,
1071+ validate_password_changed_characters)
1072+ validate_password_changed_characters_imp::validate END_SERVICE_IMPLEMENTATION();
1073+
9241074/* component provides: the validate_password service */
9251075BEGIN_COMPONENT_PROVIDES (validate_password)
9261076PROVIDES_SERVICE(validate_password, validate_password),
1077+ PROVIDES_SERVICE(validate_password, validate_password_changed_characters),
9271078 END_COMPONENT_PROVIDES();
9281079
9291080/* A block for specifying dependencies of this Component. Note that for each
9301081 dependency we need to have a placeholder, a extern to placeholder in
9311082 header file of the Component, and an entry on requires list below. */
9321083REQUIRES_SERVICE_PLACEHOLDER (log_builtins);
9331084REQUIRES_SERVICE_PLACEHOLDER (log_builtins_string);
1085+ REQUIRES_SERVICE_PLACEHOLDER (mysql_string_character_access);
9341086REQUIRES_SERVICE_PLACEHOLDER (mysql_string_factory);
9351087REQUIRES_SERVICE_PLACEHOLDER (mysql_string_case);
9361088REQUIRES_SERVICE_PLACEHOLDER (mysql_string_converter);
9371089REQUIRES_SERVICE_PLACEHOLDER (mysql_string_iterator);
9381090REQUIRES_SERVICE_PLACEHOLDER (mysql_string_ctype);
1091+ REQUIRES_SERVICE_PLACEHOLDER (mysql_string_value);
9391092REQUIRES_SERVICE_PLACEHOLDER (component_sys_variable_register);
9401093REQUIRES_SERVICE_PLACEHOLDER (component_sys_variable_unregister);
9411094REQUIRES_SERVICE_PLACEHOLDER (status_variable_registration);
@@ -950,10 +1103,11 @@ REQUIRES_MYSQL_RWLOCK_SERVICE_PLACEHOLDER;
9501103*/
9511104BEGIN_COMPONENT_REQUIRES (validate_password)
9521105REQUIRES_SERVICE(log_builtins), REQUIRES_SERVICE(log_builtins_string),
1106+ REQUIRES_SERVICE(mysql_string_character_access),
9531107 REQUIRES_SERVICE(mysql_string_factory), REQUIRES_SERVICE(mysql_string_case),
9541108 REQUIRES_SERVICE(mysql_string_converter),
9551109 REQUIRES_SERVICE(mysql_string_iterator),
956- REQUIRES_SERVICE(mysql_string_ctype),
1110+ REQUIRES_SERVICE(mysql_string_ctype), REQUIRES_SERVICE(mysql_string_value),
9571111 REQUIRES_SERVICE(component_sys_variable_register),
9581112 REQUIRES_SERVICE(component_sys_variable_unregister),
9591113 REQUIRES_SERVICE(status_variable_registration),
0 commit comments