← Back to team overview

maria-developers team mailing list archive

Re: ca4ef7185da: MDEV-9245 password "reuse prevention" validation plugin

 

Hi, Serg

On Tue, Sep 7, 2021 at 7:22 PM Sergei Golubchik <serg@xxxxxxxxxxx> wrote:

> 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@xxxxxxxxxxx
>

References