Age Owner Branch data TLA 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 "miscadmin.h"
24 : : #include "utils/builtins.h"
25 : : #include "utils/memutils.h"
26 : : #include "utils/syscache.h"
27 : : #include "utils/timestamp.h"
28 : :
29 : : /* Threshold for password expiration warnings. */
30 : : int password_expiration_warning_threshold = 604800;
31 : :
32 : : /* Enables deprecation warnings for MD5 passwords. */
33 : : bool md5_password_warnings = true;
34 : :
35 : : static bool md5_password_warning_enabled(void);
36 : :
37 : : /*
38 : : * Fetch stored password for a user, for authentication.
39 : : *
40 : : * On error, returns NULL, and stores a palloc'd string describing the reason,
41 : : * for the postmaster log, in *logdetail. The error reason should *not* be
42 : : * sent to the client, to avoid giving away user information!
43 : : */
44 : : char *
1631 michael@paquier.xyz 45 :CBC 90 : get_role_password(const char *role, const char **logdetail)
46 : : {
6149 tgl@sss.pgh.pa.us 47 : 90 : TimestampTz vuntil = 0;
48 : : HeapTuple roleTup;
49 : : Datum datum;
50 : : bool isnull;
51 : : char *shadow_pass;
52 : :
53 : : /* Get role info from pg_authid */
5980 rhaas@postgresql.org 54 : 90 : roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
6149 tgl@sss.pgh.pa.us 55 [ - + ]: 90 : if (!HeapTupleIsValid(roleTup))
56 : : {
3827 tgl@sss.pgh.pa.us 57 :UBC 0 : *logdetail = psprintf(_("Role \"%s\" does not exist."),
58 : : role);
3331 bruce@momjian.us 59 : 0 : return NULL; /* no such user */
60 : : }
61 : :
6149 tgl@sss.pgh.pa.us 62 :CBC 90 : datum = SysCacheGetAttr(AUTHNAME, roleTup,
63 : : Anum_pg_authid_rolpassword, &isnull);
64 [ - + ]: 90 : if (isnull)
65 : : {
6149 tgl@sss.pgh.pa.us 66 :UBC 0 : ReleaseSysCache(roleTup);
4537 67 : 0 : *logdetail = psprintf(_("User \"%s\" has no password assigned."),
68 : : role);
3331 bruce@momjian.us 69 : 0 : return NULL; /* user has no password */
70 : : }
3385 heikki.linnakangas@i 71 :CBC 90 : shadow_pass = TextDatumGetCString(datum);
72 : :
6149 tgl@sss.pgh.pa.us 73 : 90 : datum = SysCacheGetAttr(AUTHNAME, roleTup,
74 : : Anum_pg_authid_rolvaliduntil, &isnull);
75 [ + + ]: 90 : if (!isnull)
6149 tgl@sss.pgh.pa.us 76 :GBC 3 : vuntil = DatumGetTimestampTz(datum);
77 : :
6149 tgl@sss.pgh.pa.us 78 :CBC 90 : ReleaseSysCache(roleTup);
79 : :
80 : : /*
81 : : * Password OK, but check to be sure we are not past rolvaliduntil or
82 : : * password_expiration_warning_threshold.
83 : : */
139 nathan@postgresql.or 84 [ + + ]:GNC 90 : if (!isnull)
85 : : {
86 : 3 : TimestampTz now = GetCurrentTimestamp();
87 : 3 : uint64 expire_time = TimestampDifferenceMicroseconds(now, vuntil);
88 : :
89 : : /*
90 : : * If we're past rolvaliduntil, the connection attempt should fail, so
91 : : * update logdetail and return NULL.
92 : : */
93 [ + + ]: 3 : if (vuntil < now)
94 : : {
95 : 1 : *logdetail = psprintf(_("User \"%s\" has an expired password."),
96 : : role);
97 : 1 : return NULL;
98 : : }
99 : :
100 : : /*
101 : : * If we're past the warning threshold, the connection attempt should
102 : : * succeed, but we still want to emit a warning. To do so, we queue
103 : : * the warning message using StoreConnectionWarning() so that it will
104 : : * be emitted at the end of InitPostgres(), and we return normally.
105 : : */
106 [ + + ]: 2 : if (expire_time / USECS_PER_SEC < password_expiration_warning_threshold)
107 : : {
108 : : MemoryContext oldcontext;
109 : : int days;
110 : : int hours;
111 : : int minutes;
112 : : char *warning;
113 : : char *detail;
114 : :
115 : 1 : oldcontext = MemoryContextSwitchTo(TopMemoryContext);
116 : :
117 : 1 : days = expire_time / USECS_PER_DAY;
118 : 1 : hours = (expire_time % USECS_PER_DAY) / USECS_PER_HOUR;
119 : 1 : minutes = (expire_time % USECS_PER_HOUR) / USECS_PER_MINUTE;
120 : :
121 : 1 : warning = pstrdup(_("role password will expire soon"));
122 : :
123 [ + - ]: 1 : if (days > 0)
124 : 1 : detail = psprintf(ngettext("The password for role \"%s\" will expire in %d day.",
125 : : "The password for role \"%s\" will expire in %d days.",
126 : : days),
127 : : role, days);
139 nathan@postgresql.or 128 [ # # ]:UNC 0 : else if (hours > 0)
129 : 0 : detail = psprintf(ngettext("The password for role \"%s\" will expire in %d hour.",
130 : : "The password for role \"%s\" will expire in %d hours.",
131 : : hours),
132 : : role, hours);
133 [ # # ]: 0 : else if (minutes > 0)
134 : 0 : detail = psprintf(ngettext("The password for role \"%s\" will expire in %d minute.",
135 : : "The password for role \"%s\" will expire in %d minutes.",
136 : : minutes),
137 : : role, minutes);
138 : : else
139 : 0 : detail = psprintf(_("The password for role \"%s\" will expire in less than 1 minute."),
140 : : role);
141 : :
18 fujii@postgresql.org 142 :GNC 1 : StoreConnectionWarning(warning, detail, NULL);
143 : :
139 nathan@postgresql.or 144 : 1 : MemoryContextSwitchTo(oldcontext);
145 : : }
146 : : }
147 : :
3385 heikki.linnakangas@i 148 :CBC 89 : return shadow_pass;
149 : : }
150 : :
151 : : /*
152 : : * What kind of a password type is 'shadow_pass'?
153 : : */
154 : : PasswordType
3436 155 : 480 : get_password_type(const char *shadow_pass)
156 : : {
157 : : char *encoded_salt;
158 : : int iterations;
1288 michael@paquier.xyz 159 : 480 : int key_length = 0;
160 : : pg_cryptohash_type hash_type;
161 : : uint8 stored_key[SCRAM_MAX_KEY_LEN];
162 : : uint8 server_key[SCRAM_MAX_KEY_LEN];
163 : :
2625 164 [ + + ]: 480 : if (strncmp(shadow_pass, "md5", 3) == 0 &&
165 [ + + ]: 78 : strlen(shadow_pass) == MD5_PASSWD_LEN &&
166 [ + + ]: 70 : strspn(shadow_pass + 3, MD5_PASSWD_CHARSET) == MD5_PASSWD_LEN - 3)
3436 heikki.linnakangas@i 167 : 62 : return PASSWORD_TYPE_MD5;
1288 michael@paquier.xyz 168 [ + + ]: 418 : if (parse_scram_secret(shadow_pass, &iterations, &hash_type, &key_length,
169 : : &encoded_salt, stored_key, server_key))
3360 heikki.linnakangas@i 170 : 259 : return PASSWORD_TYPE_SCRAM_SHA_256;
3436 171 : 159 : return PASSWORD_TYPE_PLAINTEXT;
172 : : }
173 : :
174 : : /*
175 : : * Given a user-supplied password, convert it into a secret of
176 : : * 'target_type' kind.
177 : : *
178 : : * If the password is already in encrypted form, we cannot reverse the
179 : : * hash, so it is stored as it is regardless of the requested type.
180 : : */
181 : : char *
182 : 105 : encrypt_password(PasswordType target_type, const char *role,
183 : : const char *password)
184 : : {
185 : 105 : PasswordType guessed_type = get_password_type(password);
631 nathan@postgresql.or 186 : 105 : char *encrypted_password = NULL;
1631 michael@paquier.xyz 187 : 105 : const char *errstr = NULL;
188 : :
3340 heikki.linnakangas@i 189 [ + + ]: 105 : if (guessed_type != PASSWORD_TYPE_PLAINTEXT)
190 : : {
191 : : /*
192 : : * Cannot convert an already-encrypted password from one format to
193 : : * another, so return it as it is.
194 : : */
631 nathan@postgresql.or 195 : 28 : encrypted_password = pstrdup(password);
196 : : }
197 : : else
198 : : {
199 [ + + - - ]: 77 : switch (target_type)
200 : : {
201 : 15 : case PASSWORD_TYPE_MD5:
202 : 15 : encrypted_password = palloc(MD5_PASSWD_LEN + 1);
203 : :
155 peter@eisentraut.org 204 [ - + ]:GNC 15 : if (!pg_md5_encrypt(password, (const uint8 *) role, strlen(role),
205 : : encrypted_password, &errstr))
631 nathan@postgresql.or 206 [ # # ]:UBC 0 : elog(ERROR, "password encryption failed: %s", errstr);
631 nathan@postgresql.or 207 :CBC 15 : break;
208 : :
209 : 62 : case PASSWORD_TYPE_SCRAM_SHA_256:
210 : 62 : encrypted_password = pg_be_scram_build_secret(password);
211 : 62 : break;
212 : :
631 nathan@postgresql.or 213 :UBC 0 : case PASSWORD_TYPE_PLAINTEXT:
214 [ # # ]: 0 : elog(ERROR, "cannot encrypt password with 'plaintext'");
215 : : break;
216 : : }
217 : : }
218 : :
631 nathan@postgresql.or 219 [ - + ]:CBC 105 : Assert(encrypted_password);
220 : :
221 : : /*
222 : : * Valid password hashes may be very long, but we don't want to store
223 : : * anything that might need out-of-line storage, since de-TOASTing won't
224 : : * work during authentication because we haven't selected a database yet
225 : : * and cannot read pg_class. 512 bytes should be more than enough for all
226 : : * practical use, so fail for anything longer.
227 : : */
228 [ + - ]: 105 : if (encrypted_password && /* keep compiler quiet */
229 [ + + ]: 105 : strlen(encrypted_password) > MAX_ENCRYPTED_PASSWORD_LEN)
230 : : {
231 : : /*
232 : : * We don't expect any of our own hashing routines to produce hashes
233 : : * that are too long.
234 : : */
235 [ - + ]: 8 : Assert(guessed_type != PASSWORD_TYPE_PLAINTEXT);
236 : :
237 [ + - ]: 8 : ereport(ERROR,
238 : : (errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
239 : : errmsg("encrypted password is too long"),
240 : : errdetail("Encrypted passwords must be no longer than %d bytes.",
241 : : MAX_ENCRYPTED_PASSWORD_LEN)));
242 : : }
243 : :
575 244 [ + + + + ]: 191 : if (md5_password_warnings &&
245 : 94 : get_password_type(encrypted_password) == PASSWORD_TYPE_MD5)
246 [ + - ]: 23 : ereport(WARNING,
247 : : (errcode(ERRCODE_WARNING_DEPRECATED_FEATURE),
248 : : errmsg("setting an MD5-encrypted password"),
249 : : errdetail("MD5 password support is deprecated and will be removed in a future release of PostgreSQL."),
250 : : errhint("Refer to the PostgreSQL documentation for details about migrating to another password type.")));
251 : :
631 252 : 97 : return encrypted_password;
253 : : }
254 : :
255 : : /*
256 : : * Check MD5 authentication response, and return STATUS_OK or STATUS_ERROR.
257 : : *
258 : : * 'shadow_pass' is the user's correct password or password hash, as stored
259 : : * in pg_authid.rolpassword.
260 : : * 'client_pass' is the response given by the remote user to the MD5 challenge.
261 : : * 'md5_salt' is the salt used in the MD5 authentication challenge.
262 : : *
263 : : * In the error case, save a string at *logdetail that will be sent to the
264 : : * postmaster log (but not the client).
265 : : */
266 : : int
3487 heikki.linnakangas@i 267 : 2 : md5_crypt_verify(const char *role, const char *shadow_pass,
268 : : const char *client_pass,
269 : : const uint8 *md5_salt, int md5_salt_len,
270 : : const char **logdetail)
271 : : {
272 : : int retval;
273 : : char crypt_pwd[MD5_PASSWD_LEN + 1];
1631 michael@paquier.xyz 274 : 2 : const char *errstr = NULL;
275 : :
3487 heikki.linnakangas@i 276 [ - + ]: 2 : Assert(md5_salt_len > 0);
277 : :
3340 278 [ - + ]: 2 : if (get_password_type(shadow_pass) != PASSWORD_TYPE_MD5)
279 : : {
280 : : /* incompatible password hash format. */
3340 heikki.linnakangas@i 281 :UBC 0 : *logdetail = psprintf(_("User \"%s\" has a password that cannot be used with MD5 authentication."),
282 : : role);
283 : 0 : return STATUS_ERROR;
284 : : }
285 : :
286 : : /*
287 : : * Compute the correct answer for the MD5 challenge.
288 : : */
289 : : /* stored password already encrypted, only do salt */
3340 heikki.linnakangas@i 290 [ - + ]:CBC 2 : if (!pg_md5_encrypt(shadow_pass + strlen("md5"),
291 : : md5_salt, md5_salt_len,
292 : : crypt_pwd, &errstr))
293 : : {
1631 michael@paquier.xyz 294 :UBC 0 : *logdetail = errstr;
3340 heikki.linnakangas@i 295 : 0 : return STATUS_ERROR;
296 : : }
297 : :
50 michael@paquier.xyz 298 [ + - + - ]:CBC 4 : if (strlen(client_pass) == strlen(crypt_pwd) &&
299 : 2 : timingsafe_bcmp(client_pass, crypt_pwd, strlen(crypt_pwd)) == 0)
127 nathan@postgresql.or 300 :GNC 2 : {
301 : : MemoryContext oldcontext;
302 : : char *warning;
303 : : char *detail;
304 : :
3487 heikki.linnakangas@i 305 :CBC 2 : retval = STATUS_OK;
306 : :
18 fujii@postgresql.org 307 :GNC 2 : oldcontext = MemoryContextSwitchTo(TopMemoryContext);
308 : :
309 : 2 : warning = pstrdup(_("authenticated with an MD5-encrypted password"));
310 : 2 : detail = pstrdup(_("MD5 password support is deprecated and will be removed in a future release of PostgreSQL."));
311 : 2 : StoreConnectionWarning(warning, detail, md5_password_warning_enabled);
312 : :
313 : 2 : MemoryContextSwitchTo(oldcontext);
314 : : }
315 : : else
316 : : {
3487 heikki.linnakangas@i 317 :UBC 0 : *logdetail = psprintf(_("Password does not match for user \"%s\"."),
318 : : role);
319 : 0 : retval = STATUS_ERROR;
320 : : }
321 : :
3487 heikki.linnakangas@i 322 :CBC 2 : return retval;
323 : : }
324 : :
325 : : static bool
18 fujii@postgresql.org 326 :GNC 2 : md5_password_warning_enabled(void)
327 : : {
328 : 2 : return md5_password_warnings;
329 : : }
330 : :
331 : : /*
332 : : * Check given password for given user, and return STATUS_OK or STATUS_ERROR.
333 : : *
334 : : * 'shadow_pass' is the user's correct password hash, as stored in
335 : : * pg_authid.rolpassword.
336 : : * 'client_pass' is the password given by the remote user.
337 : : *
338 : : * In the error case, store a string at *logdetail that will be sent to the
339 : : * postmaster log (but not the client).
340 : : */
341 : : int
3487 heikki.linnakangas@i 342 :CBC 125 : plain_crypt_verify(const char *role, const char *shadow_pass,
343 : : const char *client_pass,
344 : : const char **logdetail)
345 : : {
346 : : char crypt_client_pass[MD5_PASSWD_LEN + 1];
1631 michael@paquier.xyz 347 : 125 : const char *errstr = NULL;
348 : :
349 : : /*
350 : : * Client sent password in plaintext. If we have an MD5 hash stored, hash
351 : : * the password the client sent, and compare the hashes. Otherwise
352 : : * compare the plaintext passwords directly.
353 : : */
3436 heikki.linnakangas@i 354 [ + + + - ]: 125 : switch (get_password_type(shadow_pass))
355 : : {
3360 356 : 32 : case PASSWORD_TYPE_SCRAM_SHA_256:
3392 357 [ + + ]: 32 : if (scram_verify_plain_password(role,
358 : : client_pass,
359 : : shadow_pass))
360 : : {
361 : 13 : return STATUS_OK;
362 : : }
363 : : else
364 : : {
365 : 19 : *logdetail = psprintf(_("Password does not match for user \"%s\"."),
366 : : role);
367 : 19 : return STATUS_ERROR;
368 : : }
369 : : break;
370 : :
3436 371 : 16 : case PASSWORD_TYPE_MD5:
372 [ - + ]: 16 : if (!pg_md5_encrypt(client_pass,
373 : : (const uint8 *) role,
374 : : strlen(role),
375 : : crypt_client_pass,
376 : : &errstr))
377 : : {
1631 michael@paquier.xyz 378 :UBC 0 : *logdetail = errstr;
3436 heikki.linnakangas@i 379 : 0 : return STATUS_ERROR;
380 : : }
50 michael@paquier.xyz 381 [ + - + + ]:CBC 32 : if (strlen(crypt_client_pass) == strlen(shadow_pass) &&
382 : 16 : timingsafe_bcmp(crypt_client_pass, shadow_pass, strlen(shadow_pass)) == 0)
3392 heikki.linnakangas@i 383 : 6 : return STATUS_OK;
384 : : else
385 : : {
386 : 10 : *logdetail = psprintf(_("Password does not match for user \"%s\"."),
387 : : role);
388 : 10 : return STATUS_ERROR;
389 : : }
390 : : break;
391 : :
3436 392 : 77 : case PASSWORD_TYPE_PLAINTEXT:
393 : :
394 : : /*
395 : : * We never store passwords in plaintext, so this shouldn't
396 : : * happen.
397 : : */
398 : 77 : break;
399 : : }
400 : :
401 : : /*
402 : : * This shouldn't happen. Plain "password" authentication is possible
403 : : * with any kind of stored password hash.
404 : : */
3392 405 : 77 : *logdetail = psprintf(_("Password of user \"%s\" is in unrecognized format."),
406 : : role);
407 : 77 : return STATUS_ERROR;
408 : : }
|