Skip to content

Commit afb7aad

Browse files
WL#15751: Password policy: Enforce minimum number of changed characters
Description: - Added new service to check for changed characters in password - Updated component_validate_password and implemented new service - Updated server component to use the server when current password is provided while changing the password - Added test cases Change-Id: I2177e3861fccef965d7e00e6267516db20281e1b
1 parent 385ccdd commit afb7aad

14 files changed

+897
-58
lines changed

components/validate_password/validate_password_imp.cc

Lines changed: 156 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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
4042
const int MAX_DICTIONARY_FILE_LENGTH = (1024 * 1024);
@@ -90,6 +92,7 @@ static char *validate_password_dictionary_file;
9092
static char *validate_password_dictionary_file_last_parsed = nullptr;
9193
static long long validate_password_dictionary_file_words_count = 0;
9294
static 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(&current_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(&current_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, &current_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+
597708
int 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

609720
int 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");
735872
check_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)
9211067
validate_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 */
9251075
BEGIN_COMPONENT_PROVIDES(validate_password)
9261076
PROVIDES_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. */
9321083
REQUIRES_SERVICE_PLACEHOLDER(log_builtins);
9331084
REQUIRES_SERVICE_PLACEHOLDER(log_builtins_string);
1085+
REQUIRES_SERVICE_PLACEHOLDER(mysql_string_character_access);
9341086
REQUIRES_SERVICE_PLACEHOLDER(mysql_string_factory);
9351087
REQUIRES_SERVICE_PLACEHOLDER(mysql_string_case);
9361088
REQUIRES_SERVICE_PLACEHOLDER(mysql_string_converter);
9371089
REQUIRES_SERVICE_PLACEHOLDER(mysql_string_iterator);
9381090
REQUIRES_SERVICE_PLACEHOLDER(mysql_string_ctype);
1091+
REQUIRES_SERVICE_PLACEHOLDER(mysql_string_value);
9391092
REQUIRES_SERVICE_PLACEHOLDER(component_sys_variable_register);
9401093
REQUIRES_SERVICE_PLACEHOLDER(component_sys_variable_unregister);
9411094
REQUIRES_SERVICE_PLACEHOLDER(status_variable_registration);
@@ -950,10 +1103,11 @@ REQUIRES_MYSQL_RWLOCK_SERVICE_PLACEHOLDER;
9501103
*/
9511104
BEGIN_COMPONENT_REQUIRES(validate_password)
9521105
REQUIRES_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),

components/validate_password/validate_password_imp.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */
3636
extern REQUIRES_SERVICE_PLACEHOLDER(registry);
3737
extern REQUIRES_SERVICE_PLACEHOLDER(log_builtins);
3838
extern REQUIRES_SERVICE_PLACEHOLDER(log_builtins_string);
39+
extern REQUIRES_SERVICE_PLACEHOLDER(mysql_string_character_access);
3940
extern REQUIRES_SERVICE_PLACEHOLDER(mysql_string_factory);
4041
extern REQUIRES_SERVICE_PLACEHOLDER(mysql_string_case);
4142
extern REQUIRES_SERVICE_PLACEHOLDER(mysql_string_converter);
4243
extern REQUIRES_SERVICE_PLACEHOLDER(mysql_string_iterator);
4344
extern REQUIRES_SERVICE_PLACEHOLDER(mysql_string_ctype);
45+
extern REQUIRES_SERVICE_PLACEHOLDER(mysql_string_value);
4446
extern REQUIRES_SERVICE_PLACEHOLDER(component_sys_variable_register);
4547
extern REQUIRES_SERVICE_PLACEHOLDER(component_sys_variable_unregister);
4648
extern REQUIRES_SERVICE_PLACEHOLDER(status_variable_registration);
@@ -69,4 +71,24 @@ class validate_password_imp {
6971
static DEFINE_BOOL_METHOD(get_strength, (void *thd, my_h_string password,
7072
unsigned int *strength));
7173
};
74+
75+
class validate_password_changed_characters_imp {
76+
public:
77+
/**
78+
Validate if number of changed characters matches the pre-configured
79+
criteria
80+
81+
@param [in] current_password Current password
82+
@param [in] new_password New password
83+
@param [out] minimum_required Minimum required number of changed characters
84+
@param [out] changed Actual number of changed characters
85+
86+
@returns Result of validation
87+
@retval false Success
88+
@retval true Error
89+
*/
90+
static DEFINE_BOOL_METHOD(validate, (my_h_string current_password,
91+
my_h_string new_password,
92+
uint *minimum_required, uint *changed));
93+
};
7294
#endif /* VALIDATE_PASSWORD_IMP_H */
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/* Copyright (c) 2023, Oracle and/or its affiliates.
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+
#ifndef COMPONENTS_SERVICES_BITS_MYSQL_STRING_BITS_H
24+
#define COMPONENTS_SERVICES_BITS_MYSQL_STRING_BITS_H
25+
26+
/**
27+
@file mysql/components/services/bits/mysql_string_bits.h
28+
Type information related to strings
29+
*/
30+
31+
#define CHAR_TYPE_UPPER_CASE (1 << 0)
32+
#define CHAR_TYPE_LOWER_CASE (1 << 1)
33+
#define CHAR_TYPE_NUMERICAL (1 << 2)
34+
#define CHAR_TYPE_SPACING (1 << 3)
35+
#define CHAR_TYPE_PUNCTUATION (1 << 4)
36+
#define CHAR_TYPE_CONTROL (1 << 5)
37+
#define CHAR_TYPE_BLANK (1 << 6)
38+
#define CHAR_TYPE_HEXADECIMAL (1 << 7)
39+
40+
#endif /* COMPONENTS_SERVICES_BITS_MYSQL_STRING_BITS_H */

include/mysql/components/services/mysql_string.h

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */
2525

2626
#include <mysql/components/service.h>
2727

28+
#include <mysql/components/services/bits/mysql_string_bits.h>
29+
2830
#include "my_inttypes.h"
2931

3032
/* clang-format off */
@@ -303,18 +305,20 @@ DECLARE_BOOL_METHOD(iterator_create,
303305
(my_h_string string, my_h_string_iterator *out_iterator));
304306

305307
/**
306-
Retrieves character code at current iterator position and advances the
307-
iterator.
308+
Retrieves character type code at current iterator position and advances the
309+
iterator. Character type code is a bit mask describing various properties.
310+
Refer to types in mysql_string_bits.h
308311
309312
@param iter String iterator object handle to advance.
310-
@param [out] out_char Pointer to 64bit value to store character to. May be
311-
NULL to omit retrieval of character and just advance the iterator.
313+
@param [out] out_ctype Pointer to 64bit value to store character type.
314+
May be NULL to omit retrieval and just advance
315+
the iterator.
312316
@return Status of performed operation
313317
@retval false success
314318
@retval true failure
315319
*/
316320
DECLARE_BOOL_METHOD(iterator_get_next,
317-
(my_h_string_iterator iter, int *out_char));
321+
(my_h_string_iterator iter, int *out_ctype));
318322

319323
/**
320324
Releases the string iterator object specified.
@@ -371,6 +375,29 @@ DECLARE_BOOL_METHOD(is_lower, (my_h_string_iterator iter, bool *out));
371375
DECLARE_BOOL_METHOD(is_digit, (my_h_string_iterator iter, bool *out));
372376
END_SERVICE_DEFINITION(mysql_string_ctype)
373377

378+
/**
379+
@ingroup group_string_component_services_inventory
380+
381+
Service for retrieving one character from a string.
382+
It relies on string iterator and access ulong representation
383+
of the character.
384+
*/
385+
BEGIN_SERVICE_DEFINITION(mysql_string_value)
386+
387+
/**
388+
Retrieves character value at current iterator position
389+
390+
@param iter String iterator object handle
391+
@param [out] out Pointer to long value to store character to
392+
393+
@return Status of performed operation
394+
@retval false success
395+
@retval true failure
396+
*/
397+
DECLARE_BOOL_METHOD(get, (my_h_string_iterator iter, ulong *out));
398+
399+
END_SERVICE_DEFINITION(mysql_string_value)
400+
374401
/* mysql_string_manipulation_v1 service. */
375402

376403
/**

0 commit comments

Comments
 (0)