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 *
43 84 : get_role_password(const char *role, const char **logdetail)
44 : {
45 84 : TimestampTz vuntil = 0;
46 : HeapTuple roleTup;
47 : Datum datum;
48 : bool isnull;
49 : char *shadow_pass;
50 :
51 : /* Get role info from pg_authid */
52 84 : roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
53 84 : if (!HeapTupleIsValid(roleTup))
54 : {
55 0 : *logdetail = psprintf(_("Role \"%s\" does not exist."),
56 : role);
57 0 : return NULL; /* no such user */
58 : }
59 :
60 84 : datum = SysCacheGetAttr(AUTHNAME, roleTup,
61 : Anum_pg_authid_rolpassword, &isnull);
62 84 : if (isnull)
63 : {
64 0 : ReleaseSysCache(roleTup);
65 0 : *logdetail = psprintf(_("User \"%s\" has no password assigned."),
66 : role);
67 0 : return NULL; /* user has no password */
68 : }
69 84 : shadow_pass = TextDatumGetCString(datum);
70 :
71 84 : datum = SysCacheGetAttr(AUTHNAME, roleTup,
72 : Anum_pg_authid_rolvaliduntil, &isnull);
73 84 : if (!isnull)
74 3 : vuntil = DatumGetTimestampTz(datum);
75 :
76 84 : ReleaseSysCache(roleTup);
77 :
78 : /*
79 : * Password OK, but check to be sure we are not past rolvaliduntil or
80 : * password_expiration_warning_threshold.
81 : */
82 84 : 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);
126 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 :
140 1 : StoreConnectionWarning(warning, detail);
141 :
142 1 : MemoryContextSwitchTo(oldcontext);
143 : }
144 : }
145 :
146 83 : return shadow_pass;
147 : }
148 :
149 : /*
150 : * What kind of a password type is 'shadow_pass'?
151 : */
152 : PasswordType
153 422 : get_password_type(const char *shadow_pass)
154 : {
155 : char *encoded_salt;
156 : int iterations;
157 422 : 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 :
162 422 : 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)
165 48 : return PASSWORD_TYPE_MD5;
166 374 : if (parse_scram_secret(shadow_pass, &iterations, &hash_type, &key_length,
167 : &encoded_salt, stored_key, server_key))
168 233 : return PASSWORD_TYPE_SCRAM_SHA_256;
169 141 : 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 90 : encrypt_password(PasswordType target_type, const char *role,
181 : const char *password)
182 : {
183 90 : PasswordType guessed_type = get_password_type(password);
184 90 : char *encrypted_password = NULL;
185 90 : const char *errstr = NULL;
186 :
187 90 : 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 : */
193 22 : encrypted_password = pstrdup(password);
194 : }
195 : else
196 : {
197 68 : switch (target_type)
198 : {
199 11 : case PASSWORD_TYPE_MD5:
200 11 : encrypted_password = palloc(MD5_PASSWD_LEN + 1);
201 :
202 11 : if (!pg_md5_encrypt(password, (const uint8 *) role, strlen(role),
203 : encrypted_password, &errstr))
204 0 : elog(ERROR, "password encryption failed: %s", errstr);
205 11 : break;
206 :
207 57 : case PASSWORD_TYPE_SCRAM_SHA_256:
208 57 : encrypted_password = pg_be_scram_build_secret(password);
209 57 : break;
210 :
211 0 : case PASSWORD_TYPE_PLAINTEXT:
212 0 : elog(ERROR, "cannot encrypt password with 'plaintext'");
213 : break;
214 : }
215 : }
216 :
217 : 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 90 : if (encrypted_password && /* keep compiler quiet */
227 90 : 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 : 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 :
242 165 : if (md5_password_warnings &&
243 81 : 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 :
250 84 : 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
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];
272 1 : const char *errstr = NULL;
273 :
274 : Assert(md5_salt_len > 0);
275 :
276 1 : if (get_password_type(shadow_pass) != PASSWORD_TYPE_MD5)
277 : {
278 : /* incompatible password hash format. */
279 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 */
288 1 : if (!pg_md5_encrypt(shadow_pass + strlen("md5"),
289 : md5_salt, md5_salt_len,
290 : crypt_pwd, &errstr))
291 : {
292 0 : *logdetail = errstr;
293 0 : return STATUS_ERROR;
294 : }
295 :
296 1 : if (strcmp(client_pass, crypt_pwd) == 0)
297 : {
298 1 : retval = STATUS_OK;
299 :
300 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 : {
317 0 : *logdetail = psprintf(_("Password does not match for user \"%s\"."),
318 : role);
319 0 : retval = STATUS_ERROR;
320 : }
321 :
322 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 108 : 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];
341 108 : 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 : */
348 108 : switch (get_password_type(shadow_pass))
349 : {
350 27 : case PASSWORD_TYPE_SCRAM_SHA_256:
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 :
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 : {
372 0 : *logdetail = errstr;
373 0 : return STATUS_ERROR;
374 : }
375 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 :
385 68 : case PASSWORD_TYPE_PLAINTEXT:
386 :
387 : /*
388 : * We never store passwords in plaintext, so this shouldn't
389 : * happen.
390 : */
391 68 : break;
392 : }
393 :
394 : /*
395 : * This shouldn't happen. Plain "password" authentication is possible
396 : * with any kind of stored password hash.
397 : */
398 68 : *logdetail = psprintf(_("Password of user \"%s\" is in unrecognized format."),
399 : role);
400 68 : return STATUS_ERROR;
401 : }
|