Line data Source code
1 : /*-------------------------------------------------------------------------
2 : *
3 : * passwordcheck.c
4 : *
5 : *
6 : * Copyright (c) 2009-2025, PostgreSQL Global Development Group
7 : *
8 : * Author: Laurenz Albe <laurenz.albe@wien.gv.at>
9 : *
10 : * IDENTIFICATION
11 : * contrib/passwordcheck/passwordcheck.c
12 : *
13 : *-------------------------------------------------------------------------
14 : */
15 : #include "postgres.h"
16 :
17 : #include <ctype.h>
18 : #include <limits.h>
19 :
20 : #ifdef USE_CRACKLIB
21 : #include <crack.h>
22 : #endif
23 :
24 : #include "commands/user.h"
25 : #include "fmgr.h"
26 : #include "libpq/crypt.h"
27 :
28 2 : PG_MODULE_MAGIC_EXT(
29 : .name = "passwordcheck",
30 : .version = PG_VERSION
31 : );
32 :
33 : /* Saved hook value */
34 : static check_password_hook_type prev_check_password_hook = NULL;
35 :
36 : /* GUC variables */
37 : static int min_password_length = 8;
38 :
39 : /*
40 : * check_password
41 : *
42 : * performs checks on an encrypted or unencrypted password
43 : * ereport's if not acceptable
44 : *
45 : * username: name of role being created or changed
46 : * password: new password (possibly already encrypted)
47 : * password_type: PASSWORD_TYPE_* code, to indicate if the password is
48 : * in plaintext or encrypted form.
49 : * validuntil_time: password expiration time, as a timestamptz Datum
50 : * validuntil_null: true if password expiration time is NULL
51 : *
52 : * This sample implementation doesn't pay any attention to the password
53 : * expiration time, but you might wish to insist that it be non-null and
54 : * not too far in the future.
55 : */
56 : static void
57 14 : check_password(const char *username,
58 : const char *shadow_pass,
59 : PasswordType password_type,
60 : Datum validuntil_time,
61 : bool validuntil_null)
62 : {
63 14 : if (prev_check_password_hook)
64 0 : prev_check_password_hook(username, shadow_pass,
65 : password_type, validuntil_time,
66 : validuntil_null);
67 :
68 14 : if (password_type != PASSWORD_TYPE_PLAINTEXT)
69 : {
70 : /*
71 : * Unfortunately we cannot perform exhaustive checks on encrypted
72 : * passwords - we are restricted to guessing. (Alternatively, we could
73 : * insist on the password being presented non-encrypted, but that has
74 : * its own security disadvantages.)
75 : *
76 : * We only check for username = password.
77 : */
78 4 : const char *logdetail = NULL;
79 :
80 4 : if (plain_crypt_verify(username, shadow_pass, username, &logdetail) == STATUS_OK)
81 2 : ereport(ERROR,
82 : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
83 : errmsg("password must not equal user name")));
84 : }
85 : else
86 : {
87 : /*
88 : * For unencrypted passwords we can perform better checks
89 : */
90 10 : const char *password = shadow_pass;
91 10 : int pwdlen = strlen(password);
92 : int i;
93 : bool pwd_has_letter,
94 : pwd_has_nonletter;
95 : #ifdef USE_CRACKLIB
96 : const char *reason;
97 : #endif
98 :
99 : /* enforce minimum length */
100 10 : if (pwdlen < min_password_length)
101 2 : ereport(ERROR,
102 : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
103 : errmsg("password is too short"),
104 : errdetail("password must be at least \"passwordcheck.min_password_length\" (%d) bytes long",
105 : min_password_length)));
106 :
107 : /* check if the password contains the username */
108 8 : if (strstr(password, username))
109 2 : ereport(ERROR,
110 : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
111 : errmsg("password must not contain user name")));
112 :
113 : /* check if the password contains both letters and non-letters */
114 6 : pwd_has_letter = false;
115 6 : pwd_has_nonletter = false;
116 100 : for (i = 0; i < pwdlen; i++)
117 : {
118 : /*
119 : * isalpha() does not work for multibyte encodings but let's
120 : * consider non-ASCII characters non-letters
121 : */
122 94 : if (isalpha((unsigned char) password[i]))
123 86 : pwd_has_letter = true;
124 : else
125 8 : pwd_has_nonletter = true;
126 : }
127 6 : if (!pwd_has_letter || !pwd_has_nonletter)
128 2 : ereport(ERROR,
129 : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
130 : errmsg("password must contain both letters and nonletters")));
131 :
132 : #ifdef USE_CRACKLIB
133 : /* call cracklib to check password */
134 : if ((reason = FascistCheck(password, CRACKLIB_DICTPATH)))
135 : ereport(ERROR,
136 : (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
137 : errmsg("password is easily cracked"),
138 : errdetail_log("cracklib diagnostic: %s", reason)));
139 : #endif
140 : }
141 :
142 : /* all checks passed, password is ok */
143 6 : }
144 :
145 : /*
146 : * Module initialization function
147 : */
148 : void
149 2 : _PG_init(void)
150 : {
151 : /* Define custom GUC variables. */
152 2 : DefineCustomIntVariable("passwordcheck.min_password_length",
153 : "Minimum allowed password length.",
154 : NULL,
155 : &min_password_length,
156 : 8,
157 : 0, INT_MAX,
158 : PGC_SUSET,
159 : GUC_UNIT_BYTE,
160 : NULL, NULL, NULL);
161 :
162 2 : MarkGUCPrefixReserved("passwordcheck");
163 :
164 : /* activate password checks when the module is loaded */
165 2 : prev_check_password_hook = check_password_hook;
166 2 : check_password_hook = check_password;
167 2 : }
|