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-2025, 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 144 : get_role_password(const char *role, const char **logdetail)
39 : {
40 144 : TimestampTz vuntil = 0;
41 : HeapTuple roleTup;
42 : Datum datum;
43 : bool isnull;
44 : char *shadow_pass;
45 :
46 : /* Get role info from pg_authid */
47 144 : roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
48 144 : if (!HeapTupleIsValid(roleTup))
49 : {
50 0 : *logdetail = psprintf(_("Role \"%s\" does not exist."),
51 : role);
52 0 : return NULL; /* no such user */
53 : }
54 :
55 144 : datum = SysCacheGetAttr(AUTHNAME, roleTup,
56 : Anum_pg_authid_rolpassword, &isnull);
57 144 : if (isnull)
58 : {
59 0 : ReleaseSysCache(roleTup);
60 0 : *logdetail = psprintf(_("User \"%s\" has no password assigned."),
61 : role);
62 0 : return NULL; /* user has no password */
63 : }
64 144 : shadow_pass = TextDatumGetCString(datum);
65 :
66 144 : datum = SysCacheGetAttr(AUTHNAME, roleTup,
67 : Anum_pg_authid_rolvaliduntil, &isnull);
68 144 : if (!isnull)
69 0 : vuntil = DatumGetTimestampTz(datum);
70 :
71 144 : ReleaseSysCache(roleTup);
72 :
73 : /*
74 : * Password OK, but check to be sure we are not past rolvaliduntil
75 : */
76 144 : if (!isnull && vuntil < GetCurrentTimestamp())
77 : {
78 0 : *logdetail = psprintf(_("User \"%s\" has an expired password."),
79 : role);
80 0 : return NULL;
81 : }
82 :
83 144 : return shadow_pass;
84 : }
85 :
86 : /*
87 : * What kind of a password type is 'shadow_pass'?
88 : */
89 : PasswordType
90 758 : get_password_type(const char *shadow_pass)
91 : {
92 : char *encoded_salt;
93 : int iterations;
94 758 : int key_length = 0;
95 : pg_cryptohash_type hash_type;
96 : uint8 stored_key[SCRAM_MAX_KEY_LEN];
97 : uint8 server_key[SCRAM_MAX_KEY_LEN];
98 :
99 758 : if (strncmp(shadow_pass, "md5", 3) == 0 &&
100 120 : strlen(shadow_pass) == MD5_PASSWD_LEN &&
101 108 : strspn(shadow_pass + 3, MD5_PASSWD_CHARSET) == MD5_PASSWD_LEN - 3)
102 96 : return PASSWORD_TYPE_MD5;
103 662 : if (parse_scram_secret(shadow_pass, &iterations, &hash_type, &key_length,
104 : &encoded_salt, stored_key, server_key))
105 404 : return PASSWORD_TYPE_SCRAM_SHA_256;
106 258 : return PASSWORD_TYPE_PLAINTEXT;
107 : }
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 166 : encrypt_password(PasswordType target_type, const char *role,
118 : const char *password)
119 : {
120 166 : PasswordType guessed_type = get_password_type(password);
121 166 : char *encrypted_password = NULL;
122 166 : const char *errstr = NULL;
123 :
124 166 : 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 42 : encrypted_password = pstrdup(password);
131 : }
132 : else
133 : {
134 124 : switch (target_type)
135 : {
136 22 : case PASSWORD_TYPE_MD5:
137 22 : encrypted_password = palloc(MD5_PASSWD_LEN + 1);
138 :
139 22 : if (!pg_md5_encrypt(password, role, strlen(role),
140 : encrypted_password, &errstr))
141 0 : elog(ERROR, "password encryption failed: %s", errstr);
142 22 : break;
143 :
144 102 : case PASSWORD_TYPE_SCRAM_SHA_256:
145 102 : encrypted_password = pg_be_scram_build_secret(password);
146 102 : break;
147 :
148 0 : case PASSWORD_TYPE_PLAINTEXT:
149 0 : elog(ERROR, "cannot encrypt password with 'plaintext'");
150 : break;
151 : }
152 166 : }
153 :
154 : 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 166 : if (encrypted_password && /* keep compiler quiet */
164 166 : 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 : Assert(guessed_type != PASSWORD_TYPE_PLAINTEXT);
171 :
172 12 : 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 : }
178 :
179 302 : if (md5_password_warnings &&
180 148 : get_password_type(encrypted_password) == PASSWORD_TYPE_MD5)
181 34 : 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 154 : return encrypted_password;
188 : }
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 2 : md5_crypt_verify(const char *role, const char *shadow_pass,
203 : const char *client_pass,
204 : const char *md5_salt, int md5_salt_len,
205 : const char **logdetail)
206 : {
207 : int retval;
208 : char crypt_pwd[MD5_PASSWD_LEN + 1];
209 2 : const char *errstr = NULL;
210 :
211 : Assert(md5_salt_len > 0);
212 :
213 2 : 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 : 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 2 : if (!pg_md5_encrypt(shadow_pass + strlen("md5"),
226 : md5_salt, md5_salt_len,
227 : crypt_pwd, &errstr))
228 : {
229 0 : *logdetail = errstr;
230 0 : return STATUS_ERROR;
231 : }
232 :
233 2 : if (strcmp(client_pass, crypt_pwd) == 0)
234 2 : retval = STATUS_OK;
235 : else
236 : {
237 0 : *logdetail = psprintf(_("Password does not match for user \"%s\"."),
238 : role);
239 0 : retval = STATUS_ERROR;
240 : }
241 :
242 2 : return retval;
243 : }
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 202 : plain_crypt_verify(const char *role, const char *shadow_pass,
257 : const char *client_pass,
258 : const char **logdetail)
259 : {
260 : char crypt_client_pass[MD5_PASSWD_LEN + 1];
261 202 : 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 202 : switch (get_password_type(shadow_pass))
269 : {
270 52 : case PASSWORD_TYPE_SCRAM_SHA_256:
271 52 : if (scram_verify_plain_password(role,
272 : client_pass,
273 : shadow_pass))
274 : {
275 24 : return STATUS_OK;
276 : }
277 : else
278 : {
279 28 : *logdetail = psprintf(_("Password does not match for user \"%s\"."),
280 : role);
281 28 : return STATUS_ERROR;
282 : }
283 : break;
284 :
285 26 : case PASSWORD_TYPE_MD5:
286 26 : if (!pg_md5_encrypt(client_pass,
287 : role,
288 : strlen(role),
289 : crypt_client_pass,
290 : &errstr))
291 : {
292 0 : *logdetail = errstr;
293 0 : return STATUS_ERROR;
294 : }
295 26 : if (strcmp(crypt_client_pass, shadow_pass) == 0)
296 10 : return STATUS_OK;
297 : else
298 : {
299 16 : *logdetail = psprintf(_("Password does not match for user \"%s\"."),
300 : role);
301 16 : return STATUS_ERROR;
302 : }
303 : break;
304 :
305 124 : case PASSWORD_TYPE_PLAINTEXT:
306 :
307 : /*
308 : * We never store passwords in plaintext, so this shouldn't
309 : * happen.
310 : */
311 124 : break;
312 : }
313 :
314 : /*
315 : * This shouldn't happen. Plain "password" authentication is possible
316 : * with any kind of stored password hash.
317 : */
318 124 : *logdetail = psprintf(_("Password of user \"%s\" is in unrecognized format."),
319 : role);
320 124 : return STATUS_ERROR;
321 : }
|