summaryrefslogtreecommitdiffstats
path: root/contrib/passwordcheck
diff options
context:
space:
mode:
Diffstat (limited to 'contrib/passwordcheck')
-rw-r--r--contrib/passwordcheck/.gitignore4
-rw-r--r--contrib/passwordcheck/Makefile24
-rw-r--r--contrib/passwordcheck/expected/passwordcheck.out19
-rw-r--r--contrib/passwordcheck/passwordcheck.c150
-rw-r--r--contrib/passwordcheck/sql/passwordcheck.sql23
5 files changed, 220 insertions, 0 deletions
diff --git a/contrib/passwordcheck/.gitignore b/contrib/passwordcheck/.gitignore
new file mode 100644
index 0000000..5dcb3ff
--- /dev/null
+++ b/contrib/passwordcheck/.gitignore
@@ -0,0 +1,4 @@
+# Generated subdirectories
+/log/
+/results/
+/tmp_check/
diff --git a/contrib/passwordcheck/Makefile b/contrib/passwordcheck/Makefile
new file mode 100644
index 0000000..006735a
--- /dev/null
+++ b/contrib/passwordcheck/Makefile
@@ -0,0 +1,24 @@
+# contrib/passwordcheck/Makefile
+
+MODULE_big = passwordcheck
+OBJS = \
+ $(WIN32RES) \
+ passwordcheck.o
+PGFILEDESC = "passwordcheck - strengthen user password checks"
+
+# uncomment the following two lines to enable cracklib support
+# PG_CPPFLAGS = -DUSE_CRACKLIB '-DCRACKLIB_DICTPATH="/usr/lib/cracklib_dict"'
+# SHLIB_LINK = -lcrack
+
+REGRESS = passwordcheck
+
+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/contrib/passwordcheck/expected/passwordcheck.out b/contrib/passwordcheck/expected/passwordcheck.out
new file mode 100644
index 0000000..e04cda6
--- /dev/null
+++ b/contrib/passwordcheck/expected/passwordcheck.out
@@ -0,0 +1,19 @@
+LOAD 'passwordcheck';
+CREATE USER regress_user1;
+-- ok
+ALTER USER regress_user1 PASSWORD 'a_nice_long_password';
+-- error: too short
+ALTER USER regress_user1 PASSWORD 'tooshrt';
+ERROR: password is too short
+-- error: contains user name
+ALTER USER regress_user1 PASSWORD 'xyzregress_user1';
+ERROR: password must not contain user name
+-- error: contains only letters
+ALTER USER regress_user1 PASSWORD 'alessnicelongpassword';
+ERROR: password must contain both letters and nonletters
+-- encrypted ok (password is "secret")
+ALTER USER regress_user1 PASSWORD 'md51a44d829a20a23eac686d9f0d258af13';
+-- error: password is user name
+ALTER USER regress_user1 PASSWORD 'md5e589150ae7d28f93333afae92b36ef48';
+ERROR: password must not equal user name
+DROP USER regress_user1;
diff --git a/contrib/passwordcheck/passwordcheck.c b/contrib/passwordcheck/passwordcheck.c
new file mode 100644
index 0000000..9d8c58d
--- /dev/null
+++ b/contrib/passwordcheck/passwordcheck.c
@@ -0,0 +1,150 @@
+/*-------------------------------------------------------------------------
+ *
+ * passwordcheck.c
+ *
+ *
+ * Copyright (c) 2009-2022, PostgreSQL Global Development Group
+ *
+ * Author: Laurenz Albe <laurenz.albe@wien.gv.at>
+ *
+ * IDENTIFICATION
+ * contrib/passwordcheck/passwordcheck.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include <ctype.h>
+
+#ifdef USE_CRACKLIB
+#include <crack.h>
+#endif
+
+#include "commands/user.h"
+#include "fmgr.h"
+#include "libpq/crypt.h"
+
+PG_MODULE_MAGIC;
+
+/* Saved hook value in case of unload */
+static check_password_hook_type prev_check_password_hook = NULL;
+
+/* passwords shorter than this will be rejected */
+#define MIN_PWD_LENGTH 8
+
+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_* code, to indicate if the password is
+ * in plaintext or encrypted form.
+ * 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 *shadow_pass,
+ PasswordType password_type,
+ Datum validuntil_time,
+ bool validuntil_null)
+{
+ if (prev_check_password_hook)
+ prev_check_password_hook(username, shadow_pass,
+ password_type, validuntil_time,
+ validuntil_null);
+
+ if (password_type != PASSWORD_TYPE_PLAINTEXT)
+ {
+ /*
+ * 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.
+ */
+ const char *logdetail = NULL;
+
+ if (plain_crypt_verify(username, shadow_pass, username, &logdetail) == STATUS_OK)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("password must not equal user name")));
+ }
+ else
+ {
+ /*
+ * For unencrypted passwords we can perform better checks
+ */
+ const char *password = shadow_pass;
+ int pwdlen = strlen(password);
+ int i;
+ bool pwd_has_letter,
+ pwd_has_nonletter;
+#ifdef USE_CRACKLIB
+ const char *reason;
+#endif
+
+ /* enforce minimum length */
+ if (pwdlen < MIN_PWD_LENGTH)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("password is too short")));
+
+ /* 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")));
+
+ /* check if the password contains both letters and non-letters */
+ pwd_has_letter = false;
+ pwd_has_nonletter = false;
+ for (i = 0; i < pwdlen; i++)
+ {
+ /*
+ * isalpha() does not work for multibyte encodings but let's
+ * consider non-ASCII characters non-letters
+ */
+ if (isalpha((unsigned char) password[i]))
+ pwd_has_letter = true;
+ else
+ pwd_has_nonletter = true;
+ }
+ if (!pwd_has_letter || !pwd_has_nonletter)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("password must contain both letters and nonletters")));
+
+#ifdef USE_CRACKLIB
+ /* call cracklib to check password */
+ if ((reason = FascistCheck(password, CRACKLIB_DICTPATH)))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+ errmsg("password is easily cracked"),
+ errdetail_log("cracklib diagnostic: %s", reason)));
+#endif
+ }
+
+ /* all checks passed, password is ok */
+}
+
+/*
+ * Module initialization function
+ */
+void
+_PG_init(void)
+{
+ /* activate password checks when the module is loaded */
+ prev_check_password_hook = check_password_hook;
+ check_password_hook = check_password;
+}
diff --git a/contrib/passwordcheck/sql/passwordcheck.sql b/contrib/passwordcheck/sql/passwordcheck.sql
new file mode 100644
index 0000000..d98796a
--- /dev/null
+++ b/contrib/passwordcheck/sql/passwordcheck.sql
@@ -0,0 +1,23 @@
+LOAD 'passwordcheck';
+
+CREATE USER regress_user1;
+
+-- ok
+ALTER USER regress_user1 PASSWORD 'a_nice_long_password';
+
+-- error: too short
+ALTER USER regress_user1 PASSWORD 'tooshrt';
+
+-- error: contains user name
+ALTER USER regress_user1 PASSWORD 'xyzregress_user1';
+
+-- error: contains only letters
+ALTER USER regress_user1 PASSWORD 'alessnicelongpassword';
+
+-- encrypted ok (password is "secret")
+ALTER USER regress_user1 PASSWORD 'md51a44d829a20a23eac686d9f0d258af13';
+
+-- error: password is user name
+ALTER USER regress_user1 PASSWORD 'md5e589150ae7d28f93333afae92b36ef48';
+
+DROP USER regress_user1;