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