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 *
45 91 : get_role_password(const char *role, const char **logdetail)
46 : {
47 91 : TimestampTz vuntil = 0;
48 : HeapTuple roleTup;
49 : Datum datum;
50 : bool isnull;
51 : char *shadow_pass;
52 :
53 : /* Get role info from pg_authid */
54 91 : roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
55 91 : if (!HeapTupleIsValid(roleTup))
56 : {
57 0 : *logdetail = psprintf(_("Role \"%s\" does not exist."),
58 : role);
59 0 : return NULL; /* no such user */
60 : }
61 :
62 91 : datum = SysCacheGetAttr(AUTHNAME, roleTup,
63 : Anum_pg_authid_rolpassword, &isnull);
64 91 : if (isnull)
65 : {
66 0 : ReleaseSysCache(roleTup);
67 0 : *logdetail = psprintf(_("User \"%s\" has no password assigned."),
68 : role);
69 0 : return NULL; /* user has no password */
70 : }
71 91 : shadow_pass = TextDatumGetCString(datum);
72 :
73 91 : datum = SysCacheGetAttr(AUTHNAME, roleTup,
74 : Anum_pg_authid_rolvaliduntil, &isnull);
75 91 : if (!isnull)
76 3 : vuntil = DatumGetTimestampTz(datum);
77 :
78 91 : ReleaseSysCache(roleTup);
79 :
80 : /*
81 : * Password OK, but check to be sure we are not past rolvaliduntil or
82 : * password_expiration_warning_threshold.
83 : */
84 91 : 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);
128 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 :
142 1 : StoreConnectionWarning(warning, detail, NULL);
143 :
144 1 : MemoryContextSwitchTo(oldcontext);
145 : }
146 : }
147 :
148 90 : return shadow_pass;
149 : }
150 :
151 : /*
152 : * What kind of a password type is 'shadow_pass'?
153 : */
154 : PasswordType
155 494 : get_password_type(const char *shadow_pass)
156 : {
157 : char *encoded_salt;
158 : int iterations;
159 494 : 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 :
164 494 : 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)
167 62 : return PASSWORD_TYPE_MD5;
168 432 : if (parse_scram_secret(shadow_pass, &iterations, &hash_type, &key_length,
169 : &encoded_salt, stored_key, server_key))
170 265 : return PASSWORD_TYPE_SCRAM_SHA_256;
171 167 : 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 109 : encrypt_password(PasswordType target_type, const char *role,
183 : const char *password)
184 : {
185 109 : PasswordType guessed_type = get_password_type(password);
186 109 : char *encrypted_password = NULL;
187 109 : const char *errstr = NULL;
188 :
189 109 : 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 : */
195 28 : encrypted_password = pstrdup(password);
196 : }
197 : else
198 : {
199 81 : switch (target_type)
200 : {
201 15 : case PASSWORD_TYPE_MD5:
202 15 : encrypted_password = palloc(MD5_PASSWD_LEN + 1);
203 :
204 15 : if (!pg_md5_encrypt(password, (const uint8 *) role, strlen(role),
205 : encrypted_password, &errstr))
206 0 : elog(ERROR, "password encryption failed: %s", errstr);
207 15 : break;
208 :
209 66 : case PASSWORD_TYPE_SCRAM_SHA_256:
210 66 : encrypted_password = pg_be_scram_build_secret(password);
211 66 : break;
212 :
213 0 : case PASSWORD_TYPE_PLAINTEXT:
214 0 : elog(ERROR, "cannot encrypt password with 'plaintext'");
215 : break;
216 : }
217 : }
218 :
219 : 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 109 : if (encrypted_password && /* keep compiler quiet */
229 109 : 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 : 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 :
244 199 : if (md5_password_warnings &&
245 98 : 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 :
252 101 : 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
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];
274 2 : const char *errstr = NULL;
275 :
276 : Assert(md5_salt_len > 0);
277 :
278 2 : if (get_password_type(shadow_pass) != PASSWORD_TYPE_MD5)
279 : {
280 : /* incompatible password hash format. */
281 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 */
290 2 : if (!pg_md5_encrypt(shadow_pass + strlen("md5"),
291 : md5_salt, md5_salt_len,
292 : crypt_pwd, &errstr))
293 : {
294 0 : *logdetail = errstr;
295 0 : return STATUS_ERROR;
296 : }
297 :
298 4 : if (strlen(client_pass) == strlen(crypt_pwd) &&
299 2 : timingsafe_bcmp(client_pass, crypt_pwd, strlen(crypt_pwd)) == 0)
300 2 : {
301 : MemoryContext oldcontext;
302 : char *warning;
303 : char *detail;
304 :
305 2 : retval = STATUS_OK;
306 :
307 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 : {
317 0 : *logdetail = psprintf(_("Password does not match for user \"%s\"."),
318 : role);
319 0 : retval = STATUS_ERROR;
320 : }
321 :
322 2 : return retval;
323 : }
324 :
325 : static bool
326 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
342 129 : 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];
347 129 : 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 : */
354 129 : switch (get_password_type(shadow_pass))
355 : {
356 32 : case PASSWORD_TYPE_SCRAM_SHA_256:
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 :
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 : {
378 0 : *logdetail = errstr;
379 0 : return STATUS_ERROR;
380 : }
381 32 : if (strlen(crypt_client_pass) == strlen(shadow_pass) &&
382 16 : timingsafe_bcmp(crypt_client_pass, shadow_pass, strlen(shadow_pass)) == 0)
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 :
392 81 : case PASSWORD_TYPE_PLAINTEXT:
393 :
394 : /*
395 : * We never store passwords in plaintext, so this shouldn't
396 : * happen.
397 : */
398 81 : break;
399 : }
400 :
401 : /*
402 : * This shouldn't happen. Plain "password" authentication is possible
403 : * with any kind of stored password hash.
404 : */
405 81 : *logdetail = psprintf(_("Password of user \"%s\" is in unrecognized format."),
406 : role);
407 81 : return STATUS_ERROR;
408 : }
|