Branch data Line data Source code
1 : : /*-------------------------------------------------------------------------
2 : : *
3 : : * crypt.c
4 : : * Functions for dealing with encrypted passwords stored in
5 : : * pg_authid.rolpassword.
6 : : *
7 : : * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
8 : : * Portions Copyright (c) 1994, Regents of the University of California
9 : : *
10 : : * src/backend/libpq/crypt.c
11 : : *
12 : : *-------------------------------------------------------------------------
13 : : */
14 : : #include "postgres.h"
15 : :
16 : : #include <unistd.h>
17 : :
18 : : #include "catalog/pg_authid.h"
19 : : #include "common/md5.h"
20 : : #include "common/scram-common.h"
21 : : #include "libpq/crypt.h"
22 : : #include "libpq/scram.h"
23 : : #include "utils/builtins.h"
24 : : #include "utils/syscache.h"
25 : : #include "utils/timestamp.h"
26 : :
27 : : /* Enables deprecation warnings for MD5 passwords. */
28 : : bool md5_password_warnings = true;
29 : :
30 : : /*
31 : : * Fetch stored password for a user, for authentication.
32 : : *
33 : : * On error, returns NULL, and stores a palloc'd string describing the reason,
34 : : * for the postmaster log, in *logdetail. The error reason should *not* be
35 : : * sent to the client, to avoid giving away user information!
36 : : */
37 : : char *
38 : 0 : get_role_password(const char *role, const char **logdetail)
39 : : {
40 : 0 : TimestampTz vuntil = 0;
41 : 0 : HeapTuple roleTup;
42 : 0 : Datum datum;
43 : 0 : bool isnull;
44 : 0 : char *shadow_pass;
45 : :
46 : : /* Get role info from pg_authid */
47 : 0 : roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
48 [ # # ]: 0 : if (!HeapTupleIsValid(roleTup))
49 : : {
50 : 0 : *logdetail = psprintf(_("Role \"%s\" does not exist."),
51 : 0 : role);
52 : 0 : return NULL; /* no such user */
53 : : }
54 : :
55 : 0 : datum = SysCacheGetAttr(AUTHNAME, roleTup,
56 : : Anum_pg_authid_rolpassword, &isnull);
57 [ # # ]: 0 : if (isnull)
58 : : {
59 : 0 : ReleaseSysCache(roleTup);
60 : 0 : *logdetail = psprintf(_("User \"%s\" has no password assigned."),
61 : 0 : role);
62 : 0 : return NULL; /* user has no password */
63 : : }
64 : 0 : shadow_pass = TextDatumGetCString(datum);
65 : :
66 : 0 : datum = SysCacheGetAttr(AUTHNAME, roleTup,
67 : : Anum_pg_authid_rolvaliduntil, &isnull);
68 [ # # ]: 0 : if (!isnull)
69 : 0 : vuntil = DatumGetTimestampTz(datum);
70 : :
71 : 0 : ReleaseSysCache(roleTup);
72 : :
73 : : /*
74 : : * Password OK, but check to be sure we are not past rolvaliduntil
75 : : */
76 [ # # # # ]: 0 : if (!isnull && vuntil < GetCurrentTimestamp())
77 : : {
78 : 0 : *logdetail = psprintf(_("User \"%s\" has an expired password."),
79 : 0 : role);
80 : 0 : return NULL;
81 : : }
82 : :
83 : 0 : return shadow_pass;
84 : 0 : }
85 : :
86 : : /*
87 : : * What kind of a password type is 'shadow_pass'?
88 : : */
89 : : PasswordType
90 : 55 : get_password_type(const char *shadow_pass)
91 : : {
92 : 55 : char *encoded_salt;
93 : 55 : int iterations;
94 : 55 : int key_length = 0;
95 : 55 : pg_cryptohash_type hash_type;
96 : 55 : uint8 stored_key[SCRAM_MAX_KEY_LEN];
97 : 55 : uint8 server_key[SCRAM_MAX_KEY_LEN];
98 : :
99 [ + + ]: 55 : if (strncmp(shadow_pass, "md5", 3) == 0 &&
100 [ + + + + ]: 15 : strlen(shadow_pass) == MD5_PASSWD_LEN &&
101 : 13 : strspn(shadow_pass + 3, MD5_PASSWD_CHARSET) == MD5_PASSWD_LEN - 3)
102 : 11 : return PASSWORD_TYPE_MD5;
103 [ + + + + ]: 88 : if (parse_scram_secret(shadow_pass, &iterations, &hash_type, &key_length,
104 : 44 : &encoded_salt, stored_key, server_key))
105 : 20 : return PASSWORD_TYPE_SCRAM_SHA_256;
106 : 24 : return PASSWORD_TYPE_PLAINTEXT;
107 : 55 : }
108 : :
109 : : /*
110 : : * Given a user-supplied password, convert it into a secret of
111 : : * 'target_type' kind.
112 : : *
113 : : * If the password is already in encrypted form, we cannot reverse the
114 : : * hash, so it is stored as it is regardless of the requested type.
115 : : */
116 : : char *
117 : 18 : encrypt_password(PasswordType target_type, const char *role,
118 : : const char *password)
119 : : {
120 : 18 : PasswordType guessed_type = get_password_type(password);
121 : 18 : char *encrypted_password = NULL;
122 : 18 : const char *errstr = NULL;
123 : :
124 [ + + ]: 18 : if (guessed_type != PASSWORD_TYPE_PLAINTEXT)
125 : : {
126 : : /*
127 : : * Cannot convert an already-encrypted password from one format to
128 : : * another, so return it as it is.
129 : : */
130 : 6 : encrypted_password = pstrdup(password);
131 : 6 : }
132 : : else
133 : : {
134 [ + - + - ]: 12 : switch (target_type)
135 : : {
136 : : case PASSWORD_TYPE_MD5:
137 : 3 : encrypted_password = palloc(MD5_PASSWD_LEN + 1);
138 : :
139 [ + - + - ]: 6 : if (!pg_md5_encrypt(password, (uint8 *) role, strlen(role),
140 : 3 : encrypted_password, &errstr))
141 [ # # # # ]: 0 : elog(ERROR, "password encryption failed: %s", errstr);
142 : 3 : break;
143 : :
144 : : case PASSWORD_TYPE_SCRAM_SHA_256:
145 : 9 : encrypted_password = pg_be_scram_build_secret(password);
146 : 9 : break;
147 : :
148 : : case PASSWORD_TYPE_PLAINTEXT:
149 [ # # # # ]: 0 : elog(ERROR, "cannot encrypt password with 'plaintext'");
150 : 0 : break;
151 : : }
152 : : }
153 : :
154 [ + - ]: 18 : Assert(encrypted_password);
155 : :
156 : : /*
157 : : * Valid password hashes may be very long, but we don't want to store
158 : : * anything that might need out-of-line storage, since de-TOASTing won't
159 : : * work during authentication because we haven't selected a database yet
160 : : * and cannot read pg_class. 512 bytes should be more than enough for all
161 : : * practical use, so fail for anything longer.
162 : : */
163 [ + - + + ]: 18 : if (encrypted_password && /* keep compiler quiet */
164 : 18 : strlen(encrypted_password) > MAX_ENCRYPTED_PASSWORD_LEN)
165 : : {
166 : : /*
167 : : * We don't expect any of our own hashing routines to produce hashes
168 : : * that are too long.
169 : : */
170 [ + - ]: 2 : Assert(guessed_type != PASSWORD_TYPE_PLAINTEXT);
171 : :
172 [ + - + - ]: 2 : ereport(ERROR,
173 : : (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
174 : : errmsg("encrypted password is too long"),
175 : : errdetail("Encrypted passwords must be no longer than %d bytes.",
176 : : MAX_ENCRYPTED_PASSWORD_LEN)));
177 : 0 : }
178 : :
179 [ + - + + ]: 16 : if (md5_password_warnings &&
180 : 16 : get_password_type(encrypted_password) == PASSWORD_TYPE_MD5)
181 [ - + + - ]: 5 : ereport(WARNING,
182 : : (errcode(ERRCODE_WARNING_DEPRECATED_FEATURE),
183 : : errmsg("setting an MD5-encrypted password"),
184 : : errdetail("MD5 password support is deprecated and will be removed in a future release of PostgreSQL."),
185 : : errhint("Refer to the PostgreSQL documentation for details about migrating to another password type.")));
186 : :
187 : 32 : return encrypted_password;
188 : 16 : }
189 : :
190 : : /*
191 : : * Check MD5 authentication response, and return STATUS_OK or STATUS_ERROR.
192 : : *
193 : : * 'shadow_pass' is the user's correct password or password hash, as stored
194 : : * in pg_authid.rolpassword.
195 : : * 'client_pass' is the response given by the remote user to the MD5 challenge.
196 : : * 'md5_salt' is the salt used in the MD5 authentication challenge.
197 : : *
198 : : * In the error case, save a string at *logdetail that will be sent to the
199 : : * postmaster log (but not the client).
200 : : */
201 : : int
202 : 0 : md5_crypt_verify(const char *role, const char *shadow_pass,
203 : : const char *client_pass,
204 : : const uint8 *md5_salt, int md5_salt_len,
205 : : const char **logdetail)
206 : : {
207 : 0 : int retval;
208 : 0 : char crypt_pwd[MD5_PASSWD_LEN + 1];
209 : 0 : const char *errstr = NULL;
210 : :
211 [ # # ]: 0 : Assert(md5_salt_len > 0);
212 : :
213 [ # # ]: 0 : if (get_password_type(shadow_pass) != PASSWORD_TYPE_MD5)
214 : : {
215 : : /* incompatible password hash format. */
216 : 0 : *logdetail = psprintf(_("User \"%s\" has a password that cannot be used with MD5 authentication."),
217 : 0 : role);
218 : 0 : return STATUS_ERROR;
219 : : }
220 : :
221 : : /*
222 : : * Compute the correct answer for the MD5 challenge.
223 : : */
224 : : /* stored password already encrypted, only do salt */
225 [ # # # # ]: 0 : if (!pg_md5_encrypt(shadow_pass + strlen("md5"),
226 : 0 : md5_salt, md5_salt_len,
227 : 0 : crypt_pwd, &errstr))
228 : : {
229 : 0 : *logdetail = errstr;
230 : 0 : return STATUS_ERROR;
231 : : }
232 : :
233 [ # # ]: 0 : if (strcmp(client_pass, crypt_pwd) == 0)
234 : 0 : retval = STATUS_OK;
235 : : else
236 : : {
237 : 0 : *logdetail = psprintf(_("Password does not match for user \"%s\"."),
238 : 0 : role);
239 : 0 : retval = STATUS_ERROR;
240 : : }
241 : :
242 : 0 : return retval;
243 : 0 : }
244 : :
245 : : /*
246 : : * Check given password for given user, and return STATUS_OK or STATUS_ERROR.
247 : : *
248 : : * 'shadow_pass' is the user's correct password hash, as stored in
249 : : * pg_authid.rolpassword.
250 : : * 'client_pass' is the password given by the remote user.
251 : : *
252 : : * In the error case, store a string at *logdetail that will be sent to the
253 : : * postmaster log (but not the client).
254 : : */
255 : : int
256 : 20 : plain_crypt_verify(const char *role, const char *shadow_pass,
257 : : const char *client_pass,
258 : : const char **logdetail)
259 : : {
260 : 20 : char crypt_client_pass[MD5_PASSWD_LEN + 1];
261 : 20 : const char *errstr = NULL;
262 : :
263 : : /*
264 : : * Client sent password in plaintext. If we have an MD5 hash stored, hash
265 : : * the password the client sent, and compare the hashes. Otherwise
266 : : * compare the plaintext passwords directly.
267 : : */
268 [ + + + ]: 20 : switch (get_password_type(shadow_pass))
269 : : {
270 : : case PASSWORD_TYPE_SCRAM_SHA_256:
271 [ + + + + ]: 10 : if (scram_verify_plain_password(role,
272 : 5 : client_pass,
273 : 5 : shadow_pass))
274 : : {
275 : 1 : return STATUS_OK;
276 : : }
277 : : else
278 : : {
279 : 8 : *logdetail = psprintf(_("Password does not match for user \"%s\"."),
280 : 4 : role);
281 : 4 : return STATUS_ERROR;
282 : : }
283 : : break;
284 : :
285 : : case PASSWORD_TYPE_MD5:
286 [ + - + - ]: 6 : if (!pg_md5_encrypt(client_pass,
287 : 3 : (uint8 *) role,
288 : 3 : strlen(role),
289 : 3 : crypt_client_pass,
290 : : &errstr))
291 : : {
292 : 0 : *logdetail = errstr;
293 : 0 : return STATUS_ERROR;
294 : : }
295 [ + + ]: 3 : if (strcmp(crypt_client_pass, shadow_pass) == 0)
296 : 1 : return STATUS_OK;
297 : : else
298 : : {
299 : 4 : *logdetail = psprintf(_("Password does not match for user \"%s\"."),
300 : 2 : role);
301 : 2 : return STATUS_ERROR;
302 : : }
303 : : break;
304 : :
305 : : case PASSWORD_TYPE_PLAINTEXT:
306 : :
307 : : /*
308 : : * We never store passwords in plaintext, so this shouldn't
309 : : * happen.
310 : : */
311 : : break;
312 : : }
313 : :
314 : : /*
315 : : * This shouldn't happen. Plain "password" authentication is possible
316 : : * with any kind of stored password hash.
317 : : */
318 : 24 : *logdetail = psprintf(_("Password of user \"%s\" is in unrecognized format."),
319 : 12 : role);
320 : 12 : return STATUS_ERROR;
321 : 20 : }
|