summaryrefslogtreecommitdiff
path: root/plugin/password_reuse_check/password_reuse_check.c
blob: 8f5973721d846a238c6bc6aa37430f6af5dfb788 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
/* 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_global.h> // for int2store
#include <stdio.h>     // for snprintf
#include <string.h>    // for memset
#include <mysql/plugin_password_validation.h>
#include <mysqld_error.h>

#define HISTORY_DB_NAME "password_reuse_check_history"

#define SQL_BUFF_LEN 2048

#define STRING_WITH_LEN(X) (X), ((size_t) (sizeof(X) - 1))

// 0 - unlimited, otherwise number of days to check
static unsigned interval= 0;

// helping string for bin_to_hex512
static char digits[]= "0123456789ABCDEF";

/**
  Store string with length

  @param to              buffer where to put the length and string
  @param from            the string to store

  @return reference on the byte after copied string
*/

static char *store_str(char *to, const MYSQL_CONST_LEX_STRING *from)
{
  int2store(to, from->length);
  memcpy(to + 2, from->str, from->length);
  return to + 2 + from->length;
}


/**
  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[((unsigned char) *str) >> 4];
    *to++= digits[((unsigned char) *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, "password_reuse_check:[%d] %s", ME_WARNING,
                  mysql_errno(mysql), mysql_error(mysql));
}


/**
  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 default current_timestamp,"
                        " 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 (mysql_real_query(mysql, query, (unsigned long) len))
  {
    unsigned int rc= mysql_errno(mysql);
    if (rc != ER_NO_SUCH_TABLE)
    {
      if (rc != ER_DUP_ENTRY)
      {
        report_sql_error(mysql);
      }
      else
      {
        // warning used to do not change error code
        my_printf_error(ER_NOT_VALID_PASSWORD,
                        "password_reuse_check: The password was already used",
                        ME_WARNING);
      }
      return 1;
    }
    if (create_table(mysql))
      return 1;
    if (mysql_real_query(mysql, query, (unsigned long) 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 +
                  (3 * 2 /* space for storing length of the strings */);
  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;
  }

  /*
    Store: username, hostname, password
    (password first to make its rewriting password in memory simplier)
  */
  store_str(store_str(store_str(buff, password), username), hostname);
  buff[key_len]= 0; // safety
  memset(hash, 0, sizeof(hash));
  my_sha512(hash, buff, key_len);
  // safety: rewrite password with zerows
  memset(buff, 0, password->length);
  if (mysql_real_connect_local(mysql) == 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 (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 (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,
  "Password history retention period in days (0 means unlimited)", NULL, NULL,
  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(password_reuse_check)
{
  MariaDB_PASSWORD_VALIDATION_PLUGIN,
  &info,
  "password_reuse_check",
  "Oleksandr Byelkin",
  "Prevent password reuse",
  PLUGIN_LICENSE_GPL,
  NULL,
  NULL,
  0x0200,
  NULL,
  sysvars,
  "2.0",
  MariaDB_PLUGIN_MATURITY_STABLE
}
maria_declare_plugin_end;