From ef952c652f33c6cf1d2c3189cd8b4755817a4698 Mon Sep 17 00:00:00 2001 From: Alexander Ermakov Date: Wed, 16 Oct 2024 18:39:40 +0300 Subject: [PATCH] Fixed build issues --- Makefile | 21 ++- README | 24 +-- arenadata_password_check.c | 298 +++++++++++++++++++++++++++++++ sql/arenadata_password_check.sql | 75 ++++++++ 4 files changed, 401 insertions(+), 17 deletions(-) create mode 100644 arenadata_password_check.c create mode 100644 sql/arenadata_password_check.sql diff --git a/Makefile b/Makefile index 85ec1b5..8b7d814 100644 --- a/Makefile +++ b/Makefile @@ -1,15 +1,26 @@ -# passwordcheck_extra/Makefile +# arenadata_password_check/Makefile -MODULE_big = passwordcheck_extra -OBJS = passwordcheck_extra.o $(WIN32RES) -PGFILEDESC = "passwordcheck_extra - strengthen user password checks" +MODULE_big = arenadata_password_check +OBJS = arenadata_password_check.o $(WIN32RES) +PGFILEDESC = "arenadata_password_check - strengthen user password checks" -REGRESS = passwordcheck_extra +REGRESS = arenadata_password_check # uncomment the following two lines to enable cracklib support # PG_CPPFLAGS = -DUSE_CRACKLIB '-DCRACKLIB_DICTPATH="/usr/lib/cracklib_dict"' # SHLIB_LINK = -lcrack +#PG_CONFIG = pg_config +#PGXS := $(shell $(PG_CONFIG) --pgxs) +#include $(PGXS) + +ifdef USE_PGXS PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) +else +subdir = contrib/passwordcheck +top_builddir = ../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/README b/README index d8696da..6cad364 100644 --- a/README +++ b/README @@ -1,14 +1,14 @@ -passwordcheck_extra +arenadata_password_check =================== Development ----------- -passwordcheck_extra is a fork of passwordcheck, contrib module to check +arenadata_password_check is a fork of passwordcheck, contrib module to check a password quality using a fork of PostgreSQL core. There are two ways to compile and install the code: -1) Copy it as contrib/passwordcheck_extra in PostgreSQL code and use the +1) Copy it as contrib/arenadata_password_check in PostgreSQL code and use the following command: make install 2) Include PostgreSQL libraries in LD_LIBRARY_PATH and use the following @@ -25,24 +25,24 @@ make installcheck USE_PGXS=1 # Run on existing server In order to install it, install the library to server, add the following parameter value to postgresql.conf and restart server. -shared_preload_libraries = '$libdir/passwordcheck_extra' +shared_preload_libraries = '$libdir/arenadata_password_check' Features -------- This module strengthens the minimum password requirement it should have at creation with a user-defined policy: -- passwordcheck_extra.special_chars, to define a list of special characters +- arenadata_password_check.special_chars, to define a list of special characters with the password needing at least one. Default is "!@#$%^&*()_+{}|<>?=". -- passwordcheck_extra.restrict_lower, to enforce the use of at least one +- arenadata_password_check.restrict_lower, to enforce the use of at least one lower-case character. -- passwordcheck_extra.restrict_upper, to enforce the use of at least one +- arenadata_password_check.restrict_upper, to enforce the use of at least one upper-case character. -- passwordcheck_extra.restrict_numbers, to enforce the use of at least +- arenadata_password_check.restrict_numbers, to enforce the use of at least one number. -- passwordcheck_extra.restrict_special, to enforce the use of at least - one special character listed in \"passwordcheck_extra.special_chars\". -- passwordcheck_extra.minimum_length, minimum length of password allowed. +- arenadata_password_check.restrict_special, to enforce the use of at least + one special character listed in \"arenadata_password_check.special_chars\". +- arenadata_password_check.minimum_length, minimum length of password allowed. Default is 8, which likely sucks. -- passwordcheck_extra.maximum_length, maximum length of password allowed. +- arenadata_password_check.maximum_length, maximum length of password allowed. Default is 15, which definitely sucks, but it is useful for tests. diff --git a/arenadata_password_check.c b/arenadata_password_check.c new file mode 100644 index 0000000..2843e99 --- /dev/null +++ b/arenadata_password_check.c @@ -0,0 +1,298 @@ +/*------------------------------------------------------------------------- + * + * arenadata_password_check.c + * + * Copyright (c) 2024, Arenadata Software LLC + * + * Author: Alexander Ermakov + * + * IDENTIFICATION + * arenadata_password_check/arenadata_password_check.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include + +#ifdef USE_CRACKLIB +#include +#endif + +#include "commands/user.h" +#include "fmgr.h" +#include "libpq/md5.h" +#include "lib/stringinfo.h" +#include "utils/guc.h" + +PG_MODULE_MAGIC; + +/* GUC variables */ +static int password_min_len = 8; +static int password_max_len = 15; +static bool password_lower_case = true; +static bool password_upper_case = true; +static bool password_special = true; +static bool password_numbers = true; +static char *password_special_chars = "!@#$%^&*()_+{}|<>?="; + +/* + * Flags to check contents of plain text passwords, which allow tracking + * of restrictions more easily. + */ +#define PASSWORD_HAS_LOWER 0x0001 /* Lower-case character */ +#define PASSWORD_HAS_UPPER 0x0002 /* Upper-case character */ +#define PASSWORD_HAS_SPECIAL 0x0004 /* Special character */ +#define PASSWORD_HAS_NUMBER 0x0008 /* Number */ + +extern void _PG_init(void); + +/* + * check_password + * + * performs checks on an encrypted or unencrypted password + * ereport's if not acceptable + * + * username: name of role being created or changed + * password: new password (possibly already encrypted) + * password_type: PASSWORD_TYPE_PLAINTEXT or PASSWORD_TYPE_MD5 (there + * could be other encryption schemes in future) + * validuntil_time: password expiration time, as a timestamptz Datum + * validuntil_null: true if password expiration time is NULL + * + * This sample implementation doesn't pay any attention to the password + * expiration time, but you might wish to insist that it be non-null and + * not too far in the future. + */ +static void +check_password(const char *username, + const char *password, + PasswordType password_type, + Datum validuntil_time, + bool validuntil_null) +{ + int pwdlen = strlen(password); + char encrypted[MD5_PASSWD_LEN + 1]; + int i; + int password_flag = 0; + + switch (password_type) + { + case PASSWORD_TYPE_MD5: + + /* + * Unfortunately we cannot perform exhaustive checks on encrypted + * passwords - we are restricted to guessing. (Alternatively, we + * could insist on the password being presented non-encrypted, but + * that has its own security disadvantages.) + * + * We only check for username = password. + */ + /* if (!pg_md5_encrypt(username, username, namelen, encrypted, &errstr)) + elog(ERROR, "password encryption failed: %s", errstr); */ + if (strcmp(password, encrypted) == 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("password must not contain user name"))); + break; + + case PASSWORD_TYPE_PLAINTEXT: + { + StringInfoData buf; + bool set_comma = false; + + /* + * For unencrypted passwords we can perform better checks. + */ + + /* enforce minimum length */ + if (pwdlen < password_min_len) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("password is too short"))); + + /* enforce maximum length */ + if (pwdlen > password_max_len) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("password is too long"))); + + /* check if the password contains the username */ + if (strstr(password, username)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("password must not contain user name"))); + + /* Scan password characters and check contents */ + for (i = 0; i < pwdlen; i++) + { + /* Check character validity */ + if (isupper((unsigned char) password[i])) + password_flag |= PASSWORD_HAS_UPPER; + else if (islower((unsigned char) password[i])) + password_flag |= PASSWORD_HAS_LOWER; + else if (isdigit((unsigned char) password[i])) + password_flag |= PASSWORD_HAS_NUMBER; + else if (strchr(password_special_chars, + (unsigned char) password[i]) != NULL) + password_flag |= PASSWORD_HAS_SPECIAL; + else + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("password contains invalid characters"))); + } + + /* Initialize error message */ + initStringInfo(&buf); + + /* Lower-case character missing? */ + if ((password_flag & PASSWORD_HAS_LOWER) == 0 && + password_lower_case) + { + appendStringInfo(&buf, "lower-case character missing"); + set_comma = true; + } + + /* Upper-case character missing? */ + if ((password_flag & PASSWORD_HAS_UPPER) == 0 && + password_upper_case) + { + if (set_comma) + appendStringInfo(&buf, ", "); + appendStringInfo(&buf, "upper-case character missing"); + set_comma = true; + } + + /* Number missing? */ + if ((password_flag & PASSWORD_HAS_NUMBER) == 0 && + password_numbers) + { + if (set_comma) + appendStringInfo(&buf, ", "); + appendStringInfo(&buf, "number missing"); + set_comma = true; + } + + /* Special character missing */ + if ((password_flag & PASSWORD_HAS_SPECIAL) == 0 && + password_special) + { + if (set_comma) + appendStringInfo(&buf, ", "); + appendStringInfo(&buf, "special character missing " + "(needs to be one listed in \"%s\")", + password_special_chars); + } + + /* + * Complain with everything lacking if anything has been + * found. + */ + if (buf.len != 0) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("Incorrect password format: %s", + buf.data))); + +#ifdef USE_CRACKLIB + /* call cracklib to check password */ + if (FascistCheck(password, CRACKLIB_DICTPATH)) + ereport(ERROR, + (errcode(ERRCODE_INVALID_PARAMETER_VALUE), + errmsg("password is easily cracked"))); +#endif + } + break; + + default: + elog(ERROR, "unrecognized password type: %d", password_type); + break; + } + + /* all checks passed, password is ok */ +} + +/* + * Entry point for parameter loading + */ +static void +arenadata_password_check_load_params(void) +{ + /* Special character set */ + DefineCustomStringVariable("arenadata_password_check.special_chars", + "Special characters defined.", + "Default value is \"!@#$%^&*()_+{}|<>?=\".", + &password_special_chars, + "!@#$%^&*()_+{}|<>?=", + PGC_SUSET, + 0, NULL, NULL, NULL); + + /* Restrict use of lower-case characters */ + DefineCustomBoolVariable("arenadata_password_check.restrict_lower", + "Enforce use of lower-case characters.", + NULL, + &password_lower_case, + true, + PGC_SUSET, + 0, NULL, NULL, NULL); + + /* Restrict use of upper-case characters */ + DefineCustomBoolVariable("arenadata_password_check.restrict_upper", + "Enforce use of upper-case characters.", + NULL, + &password_upper_case, + true, + PGC_SUSET, + 0, NULL, NULL, NULL); + + /* Restrict use of numbers */ + DefineCustomBoolVariable("arenadata_password_check.restrict_numbers", + "Enforce use of numbers.", + NULL, + &password_numbers, + true, + PGC_SUSET, + 0, NULL, NULL, NULL); + + /* Restrict use of special characters */ + DefineCustomBoolVariable("arenadata_password_check.restrict_special", + "Enforce use of special characters.", + NULL, + &password_special, + true, + PGC_SUSET, + 0, NULL, NULL, NULL); + + /* Minimum password length */ + DefineCustomIntVariable("arenadata_password_check.minimum_length", + "Minimum length of password allowed", + "Default value set to 8.", + &password_min_len, + 8, 1, 10000, + PGC_SUSET, + 0, NULL, NULL, NULL); + + /* Maximum password length */ + DefineCustomIntVariable("arenadata_password_check.maximum_length", + "Maximum length of password allowed", + "Default value set to 15, which actually sucks.", + &password_max_len, + 15, 1, 10000, + PGC_SUSET, + 0, NULL, NULL, NULL); + +/* MarkGUCPrefixReserved("arenadata_password_check"); */ +} + +/* + * Module initialization function + */ +void +_PG_init(void) +{ + /* Load library parameters */ + arenadata_password_check_load_params(); + + /* activate password checks when the module is loaded */ + check_password_hook = check_password; +} diff --git a/sql/arenadata_password_check.sql b/sql/arenadata_password_check.sql new file mode 100644 index 0000000..abf59c5 --- /dev/null +++ b/sql/arenadata_password_check.sql @@ -0,0 +1,75 @@ +-- Load the extension to enable the tests +LOAD 'arenadata_password_check'; + +-- Restrictive policy +SET arenadata_password_check.minimum_length TO 8; +SET arenadata_password_check.maximum_length TO 15; +SET arenadata_password_check.special_chars TO '%$?@'; +SET arenadata_password_check.restrict_lower TO true; +SET arenadata_password_check.restrict_upper TO true; +SET arenadata_password_check.restrict_numbers TO true; + +-- Check password policy in place +-- Password too short +CREATE ROLE regress_pwd_foo PASSWORD '01234'; +-- Password too long +CREATE ROLE regress_pwd_foo PASSWORD '01234567890123456'; +-- Invalid characters +CREATE ROLE regress_pwd_foo PASSWORD '```````````````'; + +-- Three categories missing +-- Lower-case, upper-case, special character missing +CREATE ROLE regress_pwd_foo PASSWORD '012345678901234'; +-- Number, upper-case, special character missing +CREATE ROLE regress_pwd_foo PASSWORD 'abcdefghijklmno'; +-- Number, lower-case, special character missing +CREATE ROLE regress_pwd_foo PASSWORD 'ABCDEFGHIJKLMNO'; +-- Number, lower-case, upper-case character missing +CREATE ROLE regress_pwd_foo PASSWORD '%%%%%%%%%%%%%%%'; + +-- Two categories missing +-- Number, special character missing +CREATE ROLE regress_pwd_foo PASSWORD 'abcdefghijklmnA'; +-- Upper-case character, special character missing +CREATE ROLE regress_pwd_foo PASSWORD '01234567890123a'; +-- Lower-case character, special character missing +CREATE ROLE regress_pwd_foo PASSWORD '01234567890123A'; +-- Number, upper case missing +CREATE ROLE regress_pwd_foo PASSWORD 'abcdefghijklmn%'; +-- Number, lower-case missing +CREATE ROLE regress_pwd_foo PASSWORD 'ABCDEFGHIJKLMN%'; +-- Upper-case, lower-case missing +CREATE ROLE regress_pwd_foo PASSWORD '01234567890123%'; + +-- One category missing +-- Special character missing +CREATE ROLE regress_pwd_foo PASSWORD '0123456789012aA'; +-- Upper-case missing +CREATE ROLE regress_pwd_foo PASSWORD '0123456789012a%'; +-- Lower-case missing +CREATE ROLE regress_pwd_foo PASSWORD '0123456789012A%'; +-- Number missing +CREATE ROLE regress_pwd_foo PASSWORD 'ABCDEFGHIJKLMa%'; + +-- Valid password +CREATE ROLE regress_pwd_foo PASSWORD '012345678901Aa%'; +DROP ROLE regress_pwd_foo; + +-- Policy less restrictive +SET arenadata_password_check.restrict_lower TO false; +SET arenadata_password_check.restrict_upper TO false; +SET arenadata_password_check.restrict_numbers TO false; +SET arenadata_password_check.minimum_length TO 1; +SET arenadata_password_check.maximum_length TO 100; + +-- Special character missing +CREATE ROLE regress_pwd_foo PASSWORD '012345678901Aa'; +-- Valid password +CREATE ROLE regress_pwd_foo PASSWORD '@%'; +DROP ROLE regress_pwd_foo; + +-- Even less restrictive policy +SET arenadata_password_check.restrict_special TO false; +-- Valid password +CREATE ROLE regress_pwd_foo PASSWORD 'A'; +DROP ROLE regress_pwd_foo;