Hi, Oleksandr!
you forgot to check in the result file
On Sep 07, Oleksandr Byelkin wrote:
> revision-id: ca4ef7185da (mariadb-10.6.1-79-gca4ef7185da)
> parent(s): 271afbc88e4
> author: Oleksandr Byelkin
> committer: Oleksandr Byelkin
> timestamp: 2021-09-07 14:35:05 +0200
> message:
>
> MDEV-9245 password "reuse prevention" validation plugin
>
> diff --git a/plugin/reuse_password_check/CMakeLists.txt b/plugin/reuse_password_check/CMakeLists.txt
> --- /dev/null
> +++ b/plugin/reuse_password_check/CMakeLists.txt
> @@ -0,0 +1,4 @@
> +
> +ADD_DEFINITIONS(-DMYSQL_SERVER)
as I wrote to HF, this should not be needed.
So far it is needed, otherwise I got this:
plugin/password_reuse_check/password_reuse_check.c: In function ‘validate’:
plugin/password_reuse_check/password_reuse_check.c:170:7: error: implicit declaration of function ‘mysql_real_connect_local’; did you mean ‘mysql_real_connect_cont’? [-Werror=implicit-function-declaration]
170 | if (mysql_real_connect_local(mysql, NULL, NULL, NULL, 0) == NULL)
| ^~~~~~~~~~~~~~~~~~~~~~~~
| mysql_real_connect_cont
plugin/password_reuse_check/password_reuse_check.c:170:60: error: comparison between pointer and integer [-Werror]
170 | if (mysql_real_connect_local(mysql, NULL, NULL, NULL, 0) == NULL)
| ^~
cc1: all warnings being treated as errors
[1145/1618] Building CXX object plugin...ocket.dir/handlersocket/database.cpp.o
ninja: build stopped: subcommand failed.
> +
> +MYSQL_ADD_PLUGIN(reuse_password_check reuse_password_check.c MODULE_ONLY)
Unless you have a reason why this must be "MODULE_ONLY", please remove
MODULE_ONLY and try to compile and test with -DPLUGIN_REUSE_PASSWORD_CHECK=STATIC
OK
also, I'd suggest "password_reuse_check" not "reuse_password_check".
OK
> diff --git a/plugin/reuse_password_check/reuse_password_check.c b/plugin/reuse_password_check/reuse_password_check.c
> --- /dev/null
> +++ b/plugin/reuse_password_check/reuse_password_check.c
> @@ -0,0 +1,236 @@
> +/* Copyright (c) 2021, Oleksandr Byelkin and MariaDB
> +
> + This program is free software; you can redistribute it and/or modify
> + it under the terms of the GNU General Public License as published by
> + the Free Software Foundation; version 2 of the License.
> +
> + This program is distributed in the hope that it will be useful,
> + but WITHOUT ANY WARRANTY; without even the implied warranty of
> + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
> + GNU General Public License for more details.
> +
> + You should have received a copy of the GNU General Public License
> + along with this program; if not, write to the Free Software
> + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1335 USA */
> +
> +#include <my_config.h>
> +#include <my_global.h>
> +#include <my_base.h>
> +#include <mysql/plugin_password_validation.h>
> +#include <mysql.h>
only plugin_password_validation.h should be needed here.
OK, I removed unlikely() and added only needed defines with comment for what it is needed
> +
> +
> +#include <mysqld_error.h>
this might be needed all right
> +#include <mysql/service_sha2.h>
but this isn't needed either
OK
> +
> +#define HISTORY_DB_NAME "reuse_password_check_history"
> +
> +#define SQL_BUFF_LEN 2048
> +
> +// 0 - unlimited, otherwise number of days to check
> +static unsigned interval= 0;
> +
> +// helping string for bin_to_hex512
> +static char digits[]= "0123456789ABCDEF";
> +
> +
> +/**
> + Convert string of 512 bits (64 bytes) to hex representation
> +
> + @param to pointer to the result puffer
> + (should be at least 64*2 bytes)
> + @param str pointer to 512 bits (64 bytes string)
> +*/
> +
> +static void bin_to_hex512(char *to, const unsigned char *str)
> +{
> + const unsigned char *str_end= str + (512/8);
> + for (; str != str_end; ++str)
> + {
> + *to++= digits[((uchar) *str) >> 4];
> + *to++= digits[((uchar) *str) & 0x0F];
> + }
> +}
> +
> +
> +/**
> + Send SQL error as ER_UNKNOWN_ERROR for information
> +
> + @param mysql Connection handler
> +*/
> +
> +static void report_sql_error(MYSQL *mysql)
> +{
> + my_printf_error(ER_UNKNOWN_ERROR, "[%d] %s", ME_WARNING,
> + mysql_errno(mysql), mysql_error(mysql));
may be include plugin name here?
done
> +}
> +
> +
> +/**
> + Create the history of passwords table for this plugin.
> +
> + @param mysql Connection handler
> +
> + @retval 1 - Error
> + @retval 0 - OK
> +*/
> +
> +static int create_table(MYSQL *mysql)
> +{
> + if (mysql_real_query(mysql,
> + // 512/8 = 64
> + STRING_WITH_LEN("CREATE TABLE mysql." HISTORY_DB_NAME
> + " ( hash binary(64),"
> + " time timestamp,"
make it "default current_timestamp", just to be explicit.
even though it's implicitly default current_timestamp already.
done
> + " primary key (hash), index tm (time) )"
> + " ENGINE=Aria")))
> + {
> + report_sql_error(mysql);
> + return 1;
> + }
> + return 0;
> +}
> +
> +
> +/**
> + Run this query and create table if needed
> +
> + @param mysql Connection handler
> + @param query The query to run
> + @param len length of the query text
> +
> + @retval 1 - Error
> + @retval 0 - OK
> +*/
> +
> +static int run_query_with_table_creation(MYSQL *mysql, const char *query,
> + size_t len)
> +{
> + if (unlikely(mysql_real_query(mysql, query, len)))
> + {
> + unsigned int rc= mysql_errno(mysql);
> + if (rc != ER_NO_SUCH_TABLE)
> + {
> + // suppress this error in case of try to add the same password twice
> + if (rc != ER_DUP_ENTRY)
> + report_sql_error(mysql);
> + return 1;
> + }
> + if (create_table(mysql))
> + return 1;
> + if (unlikely(mysql_real_query(mysql, query, len)))
> + {
> + report_sql_error(mysql);
> + return 1;
> + }
> + }
> + return 0;
> +}
> +
> +
> +/**
> + Password validator
> +
> + @param username User name (part of whole login name)
> + @param password Password to validate
> + @param hostname Host name (part of whole login name)
> +
> + @retval 1 - Password is not OK or an error happened
> + @retval 0 - Password is OK
> +*/
> +
> +static int validate(const MYSQL_CONST_LEX_STRING *username,
> + const MYSQL_CONST_LEX_STRING *password,
> + const MYSQL_CONST_LEX_STRING *hostname)
> +{
> + MYSQL *mysql= NULL;
> + size_t key_len= username->length + password->length + hostname->length;
> + size_t buff_len= (key_len > SQL_BUFF_LEN ? key_len : SQL_BUFF_LEN);
> + size_t len;
> + char *buff= malloc(buff_len);
> + unsigned char hash[512/8];
> + char escaped_hash[512/8*2 + 1];
> + if (!buff)
> + return 1;
> +
> + mysql= mysql_init(NULL);
> + if (!mysql)
> + {
> + free(buff);
> + return 1;
> + }
> +
> + memcpy(buff, hostname->str, hostname->length);
> + memcpy(buff + hostname->length, username->str, username->length);
> + memcpy(buff + hostname->length + username->length, password->str,
> + password->length);
> + buff[key_len]= 0;
> + bzero(hash, sizeof(hash));
> + my_sha512(hash, buff, key_len);
> + if (mysql_real_connect_local(mysql, NULL, NULL, NULL, 0) == NULL)
> + goto sql_error;
> +
> + if (interval)
> + {
> + // trim the table
> + len= snprintf(buff, buff_len,
> + "DELETE FROM mysql." HISTORY_DB_NAME
> + " WHERE time < DATE_SUB(NOW(), interval %d day)",
> + interval);
> + if (unlikely(run_query_with_table_creation(mysql, buff, len)))
> + goto sql_error;
> + }
> +
> + bin_to_hex512(escaped_hash, hash);
> + escaped_hash[512/8*2]= '\0';
> + len= snprintf(buff, buff_len,
> + "INSERT INTO mysql." HISTORY_DB_NAME "(hash) "
> + "values (x'%s')",
> + escaped_hash);
> + if (unlikely(run_query_with_table_creation(mysql, buff, len)))
> + goto sql_error;
> +
> + free(buff);
> + mysql_close(mysql);
> + return 0; // OK
> +
> +sql_error:
> + free(buff);
> + if (mysql)
> + mysql_close(mysql);
> + return 1; // Error
> +}
> +
> +static MYSQL_SYSVAR_UINT(interval, interval, PLUGIN_VAR_RQCMDARG,
> + "How old (in days) passwords to check (0 means unlimited)", NULL, NULL,
better "Password history retention period in days (0 means unlimited)"
OK
>
> + 0, 0, 365*100, 1);
> +
> +
> +static struct st_mysql_sys_var* sysvars[]= {
> + MYSQL_SYSVAR(interval),
> + NULL
> +};
> +
> +static struct st_mariadb_password_validation info=
> +{
> + MariaDB_PASSWORD_VALIDATION_INTERFACE_VERSION,
> + validate
> +};
> +
> +maria_declare_plugin(simple_password_check)
^^^ must be the same name as in CMakeLists.txt,
otherwise static linking won't work.
fixed
> +{
> + MariaDB_PASSWORD_VALIDATION_PLUGIN,
> + &info,
> + "reuse_password_check",
> + "Oleksandr Byelkin",
> + "Reusage of password check",
Better "Password reuse check"
or, as it's a description, "Prevent password reuse"
done
> + PLUGIN_LICENSE_GPL,
> + NULL,
> + NULL,
> + 0x0100,
> + NULL,
> + sysvars,
> + "1.0",
> + MariaDB_PLUGIN_MATURITY_ALPHA
> +}
> +maria_declare_plugin_end;
>
> diff --git a/mysql-test/suite/plugins/t/reuse_password_check.test b/mysql-test/suite/plugins/t/reuse_password_check.test
> --- /dev/null
> +++ b/mysql-test/suite/plugins/t/reuse_password_check.test
> @@ -0,0 +1,58 @@
> +--source include/not_embedded.inc
> +
> +if (!$REUSE_PASSWORD_CHECK_SO) {
> + skip No REUSE_PASSWORD_CHECK plugin;
> +}
> +
> +install soname "reuse_password_check";
> +
> +set @save_reuse_password_check_interval= @@reuse_password_check_interval;
> +
> +set global reuse_password_check_interval= 0;
> +
> +--echo # Default value (sould be unlimited i.e. 0)
> +SHOW GLOBAL VARIABLES like "reuse_password_check%";
> +
> +--echo # insert user
> +grant select on *.* to user_name@localhost identified by 'test_pwd';
what about sql mode that doesn't allow to create users with grant?
I don't see where you've disabled that.
good question; I have no idea why it works, now I am trying to figure it out.
> +
> +#--error ER_NOT_VALID_PASSWORD
> +#grant select on *.* to user_name@localhost identified by 'test_pwd';
> +#show warnings;
why is that commented out?
fixed
> +
> +--error ER_CANNOT_USER
> +alter user user_name@localhost identified by 'test_pwd';
> +show warnings;
> +
> +# Plugin does not work for it
> +#--error ER_NOT_VALID_PASSWORD
> +#SET PASSWORD FOR user_name@localhost = PASSWORD('test_pwd');
> +
> +--echo # check exparation
> +
> +set global reuse_password_check_interval= 10;
> +
> +--error ER_CANNOT_USER
> +alter user user_name@localhost identified by 'test_pwd';
> +show warnings;
> +select hex(hash) from mysql.reuse_password_check_history;
> +
> +--echo # emulate old password
> +update mysql.reuse_password_check_history set time= date_sub(now(), interval
> +11 day);
> +
> +alter user user_name@localhost identified by 'test_pwd';
> +show warnings;
> +
> +drop user user_name@localhost;
> +
> +show create table mysql.reuse_password_check_history;
> +select count(*) from mysql.reuse_password_check_history;
> +
> +drop table mysql.reuse_password_check_history;
> +
> +set @save_reuse_password_check_interval= @@reuse_password_check_interval;
> +
> +set global reuse_password_check_interval= @save_reuse_password_check_interval;
> +
> +uninstall plugin reuse_password_check;
Regards,
Sergei
VP of MariaDB Server Engineering
and security@mariadb.org