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