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