Age Owner Branch data TLA 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 : :
164 tgl@sss.pgh.pa.us 28 :CBC 1 : 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
5771 57 : 7 : check_password(const char *username,
58 : : const char *shadow_pass,
59 : : PasswordType password_type,
60 : : Datum validuntil_time,
61 : : bool validuntil_null)
62 : : {
2228 michael@paquier.xyz 63 [ - + ]: 7 : if (prev_check_password_hook)
2228 michael@paquier.xyz 64 :UBC 0 : prev_check_password_hook(username, shadow_pass,
65 : : password_type, validuntil_time,
66 : : validuntil_null);
67 : :
3139 heikki.linnakangas@i 68 [ + + ]:CBC 7 : 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 : : */
1334 michael@paquier.xyz 78 : 2 : const char *logdetail = NULL;
79 : :
3139 heikki.linnakangas@i 80 [ + + ]: 2 : if (plain_crypt_verify(username, shadow_pass, username, &logdetail) == STATUS_OK)
81 [ + - ]: 1 : 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 : 5 : const char *password = shadow_pass;
91 : 5 : 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 */
242 nathan@postgresql.or 100 [ + + ]: 5 : if (pwdlen < min_password_length)
3139 heikki.linnakangas@i 101 [ + - ]: 1 : 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 [ + + ]: 4 : if (strstr(password, username))
109 [ + - ]: 1 : 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 : 3 : pwd_has_letter = false;
115 : 3 : pwd_has_nonletter = false;
116 [ + + ]: 50 : 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 [ + + ]: 47 : if (isalpha((unsigned char) password[i]))
123 : 43 : pwd_has_letter = true;
124 : : else
125 : 4 : pwd_has_nonletter = true;
126 : : }
127 [ + - + + ]: 3 : if (!pwd_has_letter || !pwd_has_nonletter)
128 [ + - ]: 1 : 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 */
5771 tgl@sss.pgh.pa.us 143 : 3 : }
144 : :
145 : : /*
146 : : * Module initialization function
147 : : */
148 : : void
149 : 1 : _PG_init(void)
150 : : {
151 : : /* Define custom GUC variables. */
242 nathan@postgresql.or 152 : 1 : 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 : 1 : MarkGUCPrefixReserved("passwordcheck");
163 : :
164 : : /* activate password checks when the module is loaded */
2228 michael@paquier.xyz 165 : 1 : prev_check_password_hook = check_password_hook;
5771 tgl@sss.pgh.pa.us 166 : 1 : check_password_hook = check_password;
167 : 1 : }
|