Skip to content

Commit bc82b6c

Browse files
committed
MDEV-9245 password "reuse prevention" validation plugin
1 parent 9d1a866 commit bc82b6c

File tree

4 files changed

+379
-0
lines changed

4 files changed

+379
-0
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
install soname "password_reuse_check";
2+
set global password_reuse_check_interval= 0;
3+
# Default value (sould be unlimited i.e. 0)
4+
SHOW GLOBAL VARIABLES like "password_reuse_check%";
5+
Variable_name Value
6+
password_reuse_check_interval 0
7+
# insert user
8+
grant select on *.* to user_name@localhost identified by 'test_pwd';
9+
grant select on *.* to user_name@localhost identified by 'test_pwd';
10+
ERROR HY000: Your password does not satisfy the current policy requirements
11+
show warnings;
12+
Level Code Message
13+
Error 1819 Your password does not satisfy the current policy requirements
14+
alter user user_name@localhost identified by 'test_pwd';
15+
ERROR HY000: Operation ALTER USER failed for 'user_name'@'localhost'
16+
show warnings;
17+
Level Code Message
18+
Error 1819 Your password does not satisfy the current policy requirements
19+
Error 1396 Operation ALTER USER failed for 'user_name'@'localhost'
20+
# check exparation
21+
set global password_reuse_check_interval= 10;
22+
alter user user_name@localhost identified by 'test_pwd';
23+
ERROR HY000: Operation ALTER USER failed for 'user_name'@'localhost'
24+
show warnings;
25+
Level Code Message
26+
Error 1819 Your password does not satisfy the current policy requirements
27+
Error 1396 Operation ALTER USER failed for 'user_name'@'localhost'
28+
select hex(hash) from mysql.password_reuse_check_history;
29+
hex(hash)
30+
6276C87127F2B65FC6B24E94E324A02FF0D393D7FB7DEAF6F5F49F0A8AB006711D5C6EF67E36A251AB6337E7E20D312F9ED66D70EB699A6EC85B1E0BC7F376C0
31+
# emulate old password
32+
update mysql.password_reuse_check_history set time= date_sub(now(), interval
33+
11 day);
34+
alter user user_name@localhost identified by 'test_pwd';
35+
show warnings;
36+
Level Code Message
37+
drop user user_name@localhost;
38+
show create table mysql.password_reuse_check_history;
39+
Table Create Table
40+
password_reuse_check_history CREATE TABLE `password_reuse_check_history` (
41+
`hash` binary(64) NOT NULL,
42+
`time` timestamp NOT NULL DEFAULT current_timestamp(),
43+
PRIMARY KEY (`hash`),
44+
KEY `tm` (`time`)
45+
) ENGINE=Aria DEFAULT CHARSET=latin1 PAGE_CHECKSUM=1
46+
select count(*) from mysql.password_reuse_check_history;
47+
count(*)
48+
1
49+
drop table mysql.password_reuse_check_history;
50+
# test error messages
51+
set global password_reuse_check_interval= 0;
52+
drop table if exists mysql.password_reuse_check_history;
53+
Warnings:
54+
Note 1051 Unknown table 'mysql.password_reuse_check_history'
55+
# test error messages
56+
create table mysql.password_reuse_check_history (wrong_structure int);
57+
grant select on *.* to user_name@localhost identified by 'test_pwd';
58+
ERROR HY000: Your password does not satisfy the current policy requirements
59+
show warnings;
60+
Level Code Message
61+
Warning 1105 password_reuse_check:[1054] Unknown column 'hash' in 'field list'
62+
Error 1819 Your password does not satisfy the current policy requirements
63+
set global password_reuse_check_interval= 10;
64+
grant select on *.* to user_name@localhost identified by 'test_pwd';
65+
ERROR HY000: Your password does not satisfy the current policy requirements
66+
show warnings;
67+
Level Code Message
68+
Warning 1105 password_reuse_check:[1054] Unknown column 'time' in 'where clause'
69+
Error 1819 Your password does not satisfy the current policy requirements
70+
drop table mysql.password_reuse_check_history;
71+
uninstall plugin password_reuse_check;
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
--source include/not_embedded.inc
2+
3+
if (!$PASSWORD_REUSE_CHECK_SO) {
4+
skip No PASSWORD_REUSE_CHECK plugin;
5+
}
6+
7+
install soname "password_reuse_check";
8+
9+
set global password_reuse_check_interval= 0;
10+
11+
--echo # Default value (sould be unlimited i.e. 0)
12+
SHOW GLOBAL VARIABLES like "password_reuse_check%";
13+
14+
--echo # insert user
15+
grant select on *.* to user_name@localhost identified by 'test_pwd';
16+
17+
--error ER_NOT_VALID_PASSWORD
18+
grant select on *.* to user_name@localhost identified by 'test_pwd';
19+
show warnings;
20+
21+
--error ER_CANNOT_USER
22+
alter user user_name@localhost identified by 'test_pwd';
23+
show warnings;
24+
25+
# Plugin does not work for it
26+
#--error ER_NOT_VALID_PASSWORD
27+
#SET PASSWORD FOR user_name@localhost = PASSWORD('test_pwd');
28+
29+
--echo # check exparation
30+
31+
set global password_reuse_check_interval= 10;
32+
33+
--error ER_CANNOT_USER
34+
alter user user_name@localhost identified by 'test_pwd';
35+
show warnings;
36+
select hex(hash) from mysql.password_reuse_check_history;
37+
38+
--echo # emulate old password
39+
update mysql.password_reuse_check_history set time= date_sub(now(), interval
40+
11 day);
41+
42+
alter user user_name@localhost identified by 'test_pwd';
43+
show warnings;
44+
45+
drop user user_name@localhost;
46+
47+
show create table mysql.password_reuse_check_history;
48+
select count(*) from mysql.password_reuse_check_history;
49+
50+
drop table mysql.password_reuse_check_history;
51+
52+
--echo # test error messages
53+
54+
set global password_reuse_check_interval= 0;
55+
56+
drop table if exists mysql.password_reuse_check_history;
57+
58+
--echo # test error messages
59+
60+
create table mysql.password_reuse_check_history (wrong_structure int);
61+
62+
--error ER_NOT_VALID_PASSWORD
63+
grant select on *.* to user_name@localhost identified by 'test_pwd';
64+
show warnings;
65+
66+
set global password_reuse_check_interval= 10;
67+
68+
--error ER_NOT_VALID_PASSWORD
69+
grant select on *.* to user_name@localhost identified by 'test_pwd';
70+
show warnings;
71+
72+
drop table mysql.password_reuse_check_history;
73+
uninstall plugin password_reuse_check;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
2+
MYSQL_ADD_PLUGIN(password_reuse_check password_reuse_check.c)
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
/* Copyright (c) 2021, Oleksandr Byelkin and MariaDB
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 as published by
5+
the Free Software Foundation; version 2 of the License.
6+
7+
This program is distributed in the hope that it will be useful,
8+
but WITHOUT ANY WARRANTY; without even the implied warranty of
9+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10+
GNU General Public License for more details.
11+
12+
You should have received a copy of the GNU General Public License
13+
along with this program; if not, write to the Free Software
14+
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */
15+
16+
#include <stdio.h> // for snprintf
17+
#include <string.h> // for memset
18+
#include <mysql/plugin_password_validation.h>
19+
#include <mysqld_error.h>
20+
21+
#define HISTORY_DB_NAME "password_reuse_check_history"
22+
23+
#define SQL_BUFF_LEN 2048
24+
25+
#define STRING_WITH_LEN(X) (X), ((size_t) (sizeof(X) - 1))
26+
27+
// 0 - unlimited, otherwise number of days to check
28+
static unsigned interval= 0;
29+
30+
// helping string for bin_to_hex512
31+
static char digits[]= "0123456789ABCDEF";
32+
33+
34+
/**
35+
Convert string of 512 bits (64 bytes) to hex representation
36+
37+
@param to pointer to the result puffer
38+
(should be at least 64*2 bytes)
39+
@param str pointer to 512 bits (64 bytes string)
40+
*/
41+
42+
static void bin_to_hex512(char *to, const unsigned char *str)
43+
{
44+
const unsigned char *str_end= str + (512/8);
45+
for (; str != str_end; ++str)
46+
{
47+
*to++= digits[((unsigned char) *str) >> 4];
48+
*to++= digits[((unsigned char) *str) & 0x0F];
49+
}
50+
}
51+
52+
53+
/**
54+
Send SQL error as ER_UNKNOWN_ERROR for information
55+
56+
@param mysql Connection handler
57+
*/
58+
59+
static void report_sql_error(MYSQL *mysql)
60+
{
61+
my_printf_error(ER_UNKNOWN_ERROR, "password_reuse_check:[%d] %s", ME_WARNING,
62+
mysql_errno(mysql), mysql_error(mysql));
63+
}
64+
65+
66+
/**
67+
Create the history of passwords table for this plugin.
68+
69+
@param mysql Connection handler
70+
71+
@retval 1 - Error
72+
@retval 0 - OK
73+
*/
74+
75+
static int create_table(MYSQL *mysql)
76+
{
77+
if (mysql_real_query(mysql,
78+
// 512/8 = 64
79+
STRING_WITH_LEN("CREATE TABLE mysql." HISTORY_DB_NAME
80+
" ( hash binary(64),"
81+
" time timestamp default current_timestamp,"
82+
" primary key (hash), index tm (time) )"
83+
" ENGINE=Aria")))
84+
{
85+
report_sql_error(mysql);
86+
return 1;
87+
}
88+
return 0;
89+
}
90+
91+
92+
/**
93+
Run this query and create table if needed
94+
95+
@param mysql Connection handler
96+
@param query The query to run
97+
@param len length of the query text
98+
99+
@retval 1 - Error
100+
@retval 0 - OK
101+
*/
102+
103+
static int run_query_with_table_creation(MYSQL *mysql, const char *query,
104+
size_t len)
105+
{
106+
if (mysql_real_query(mysql, query, len))
107+
{
108+
unsigned int rc= mysql_errno(mysql);
109+
if (rc != ER_NO_SUCH_TABLE)
110+
{
111+
// suppress this error in case of try to add the same password twice
112+
if (rc != ER_DUP_ENTRY)
113+
report_sql_error(mysql);
114+
return 1;
115+
}
116+
if (create_table(mysql))
117+
return 1;
118+
if (mysql_real_query(mysql, query, len))
119+
{
120+
report_sql_error(mysql);
121+
return 1;
122+
}
123+
}
124+
return 0;
125+
}
126+
127+
128+
/**
129+
Password validator
130+
131+
@param username User name (part of whole login name)
132+
@param password Password to validate
133+
@param hostname Host name (part of whole login name)
134+
135+
@retval 1 - Password is not OK or an error happened
136+
@retval 0 - Password is OK
137+
*/
138+
139+
static int validate(const MYSQL_CONST_LEX_STRING *username,
140+
const MYSQL_CONST_LEX_STRING *password,
141+
const MYSQL_CONST_LEX_STRING *hostname)
142+
{
143+
MYSQL *mysql= NULL;
144+
size_t key_len= username->length + password->length + hostname->length;
145+
size_t buff_len= (key_len > SQL_BUFF_LEN ? key_len : SQL_BUFF_LEN);
146+
size_t len;
147+
char *buff= malloc(buff_len);
148+
unsigned char hash[512/8];
149+
char escaped_hash[512/8*2 + 1];
150+
if (!buff)
151+
return 1;
152+
153+
mysql= mysql_init(NULL);
154+
if (!mysql)
155+
{
156+
free(buff);
157+
return 1;
158+
}
159+
160+
memcpy(buff, hostname->str, hostname->length);
161+
memcpy(buff + hostname->length, username->str, username->length);
162+
memcpy(buff + hostname->length + username->length, password->str,
163+
password->length);
164+
buff[key_len]= 0;
165+
memset(hash, 0, sizeof(hash));
166+
my_sha512(hash, buff, key_len);
167+
if (mysql_real_connect_local(mysql) == NULL)
168+
goto sql_error;
169+
170+
if (interval)
171+
{
172+
// trim the table
173+
len= snprintf(buff, buff_len,
174+
"DELETE FROM mysql." HISTORY_DB_NAME
175+
" WHERE time < DATE_SUB(NOW(), interval %d day)",
176+
interval);
177+
if (run_query_with_table_creation(mysql, buff, len))
178+
goto sql_error;
179+
}
180+
181+
bin_to_hex512(escaped_hash, hash);
182+
escaped_hash[512/8*2]= '\0';
183+
len= snprintf(buff, buff_len,
184+
"INSERT INTO mysql." HISTORY_DB_NAME "(hash) "
185+
"values (x'%s')",
186+
escaped_hash);
187+
if (run_query_with_table_creation(mysql, buff, len))
188+
goto sql_error;
189+
190+
free(buff);
191+
mysql_close(mysql);
192+
return 0; // OK
193+
194+
sql_error:
195+
free(buff);
196+
if (mysql)
197+
mysql_close(mysql);
198+
return 1; // Error
199+
}
200+
201+
static MYSQL_SYSVAR_UINT(interval, interval, PLUGIN_VAR_RQCMDARG,
202+
"Password history retention period in days (0 means unlimited)", NULL, NULL,
203+
0, 0, 365*100, 1);
204+
205+
206+
static struct st_mysql_sys_var* sysvars[]= {
207+
MYSQL_SYSVAR(interval),
208+
NULL
209+
};
210+
211+
static struct st_mariadb_password_validation info=
212+
{
213+
MariaDB_PASSWORD_VALIDATION_INTERFACE_VERSION,
214+
validate
215+
};
216+
217+
maria_declare_plugin(password_reuse_check)
218+
{
219+
MariaDB_PASSWORD_VALIDATION_PLUGIN,
220+
&info,
221+
"password_reuse_check",
222+
"Oleksandr Byelkin",
223+
"Prevent password reuse",
224+
PLUGIN_LICENSE_GPL,
225+
NULL,
226+
NULL,
227+
0x0100,
228+
NULL,
229+
sysvars,
230+
"1.0",
231+
MariaDB_PLUGIN_MATURITY_ALPHA
232+
}
233+
maria_declare_plugin_end;

0 commit comments

Comments
 (0)