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-2024, 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 : :
28 : : /*
29 : : * Fetch stored password for a user, for authentication.
30 : : *
31 : : * On error, returns NULL, and stores a palloc'd string describing the reason,
32 : : * for the postmaster log, in *logdetail. The error reason should *not* be
33 : : * sent to the client, to avoid giving away user information!
34 : : */
35 : : char *
824 michael@paquier.xyz 36 :CBC 64 : get_role_password(const char *role, const char **logdetail)
37 : : {
5342 tgl@sss.pgh.pa.us 38 : 64 : TimestampTz vuntil = 0;
39 : : HeapTuple roleTup;
40 : : Datum datum;
41 : : bool isnull;
42 : : char *shadow_pass;
43 : :
44 : : /* Get role info from pg_authid */
5173 rhaas@postgresql.org 45 : 64 : roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
5342 tgl@sss.pgh.pa.us 46 [ - + ]: 64 : if (!HeapTupleIsValid(roleTup))
47 : : {
3020 tgl@sss.pgh.pa.us 48 :UBC 0 : *logdetail = psprintf(_("Role \"%s\" does not exist."),
49 : : role);
2524 bruce@momjian.us 50 : 0 : return NULL; /* no such user */
51 : : }
52 : :
5342 tgl@sss.pgh.pa.us 53 :CBC 64 : datum = SysCacheGetAttr(AUTHNAME, roleTup,
54 : : Anum_pg_authid_rolpassword, &isnull);
55 [ - + ]: 64 : if (isnull)
56 : : {
5342 tgl@sss.pgh.pa.us 57 :UBC 0 : ReleaseSysCache(roleTup);
3730 58 : 0 : *logdetail = psprintf(_("User \"%s\" has no password assigned."),
59 : : role);
2524 bruce@momjian.us 60 : 0 : return NULL; /* user has no password */
61 : : }
2578 heikki.linnakangas@i 62 :CBC 64 : shadow_pass = TextDatumGetCString(datum);
63 : :
5342 tgl@sss.pgh.pa.us 64 : 64 : datum = SysCacheGetAttr(AUTHNAME, roleTup,
65 : : Anum_pg_authid_rolvaliduntil, &isnull);
66 [ - + ]: 64 : if (!isnull)
5342 tgl@sss.pgh.pa.us 67 :UBC 0 : vuntil = DatumGetTimestampTz(datum);
68 : :
5342 tgl@sss.pgh.pa.us 69 :CBC 64 : ReleaseSysCache(roleTup);
70 : :
71 : : /*
72 : : * Password OK, but check to be sure we are not past rolvaliduntil
73 : : */
2578 heikki.linnakangas@i 74 [ - + - - ]: 64 : if (!isnull && vuntil < GetCurrentTimestamp())
75 : : {
2680 heikki.linnakangas@i 76 :UBC 0 : *logdetail = psprintf(_("User \"%s\" has an expired password."),
77 : : role);
2578 78 : 0 : return NULL;
79 : : }
80 : :
2578 heikki.linnakangas@i 81 :CBC 64 : return shadow_pass;
82 : : }
83 : :
84 : : /*
85 : : * What kind of a password type is 'shadow_pass'?
86 : : */
87 : : PasswordType
2629 88 : 264 : get_password_type(const char *shadow_pass)
89 : : {
90 : : char *encoded_salt;
91 : : int iterations;
481 michael@paquier.xyz 92 : 264 : int key_length = 0;
93 : : pg_cryptohash_type hash_type;
94 : : uint8 stored_key[SCRAM_MAX_KEY_LEN];
95 : : uint8 server_key[SCRAM_MAX_KEY_LEN];
96 : :
1818 97 [ + + ]: 264 : if (strncmp(shadow_pass, "md5", 3) == 0 &&
98 [ + + ]: 43 : strlen(shadow_pass) == MD5_PASSWD_LEN &&
99 [ + + ]: 37 : strspn(shadow_pass + 3, MD5_PASSWD_CHARSET) == MD5_PASSWD_LEN - 3)
2629 heikki.linnakangas@i 100 : 31 : return PASSWORD_TYPE_MD5;
481 michael@paquier.xyz 101 [ + + ]: 233 : if (parse_scram_secret(shadow_pass, &iterations, &hash_type, &key_length,
102 : : &encoded_salt, stored_key, server_key))
2553 heikki.linnakangas@i 103 : 115 : return PASSWORD_TYPE_SCRAM_SHA_256;
2629 104 : 118 : return PASSWORD_TYPE_PLAINTEXT;
105 : : }
106 : :
107 : : /*
108 : : * Given a user-supplied password, convert it into a secret of
109 : : * 'target_type' kind.
110 : : *
111 : : * If the password is already in encrypted form, we cannot reverse the
112 : : * hash, so it is stored as it is regardless of the requested type.
113 : : */
114 : : char *
115 : 71 : encrypt_password(PasswordType target_type, const char *role,
116 : : const char *password)
117 : : {
118 : 71 : PasswordType guessed_type = get_password_type(password);
119 : : char *encrypted_password;
824 michael@paquier.xyz 120 : 71 : const char *errstr = NULL;
121 : :
2533 heikki.linnakangas@i 122 [ + + ]: 71 : if (guessed_type != PASSWORD_TYPE_PLAINTEXT)
123 : : {
124 : : /*
125 : : * Cannot convert an already-encrypted password from one format to
126 : : * another, so return it as it is.
127 : : */
128 : 14 : return pstrdup(password);
129 : : }
130 : :
131 [ + + - - ]: 57 : switch (target_type)
132 : : {
2629 133 : 11 : case PASSWORD_TYPE_MD5:
2533 134 : 11 : encrypted_password = palloc(MD5_PASSWD_LEN + 1);
135 : :
136 [ - + ]: 11 : if (!pg_md5_encrypt(password, role, strlen(role),
137 : : encrypted_password, &errstr))
824 michael@paquier.xyz 138 [ # # ]:UBC 0 : elog(ERROR, "password encryption failed: %s", errstr);
2533 heikki.linnakangas@i 139 :CBC 11 : return encrypted_password;
140 : :
2553 141 : 46 : case PASSWORD_TYPE_SCRAM_SHA_256:
1646 peter@eisentraut.org 142 : 46 : return pg_be_scram_build_secret(password);
143 : :
2533 heikki.linnakangas@i 144 :UBC 0 : case PASSWORD_TYPE_PLAINTEXT:
145 [ # # ]: 0 : elog(ERROR, "cannot encrypt password with 'plaintext'");
146 : : }
147 : :
148 : : /*
149 : : * This shouldn't happen, because the above switch statements should
150 : : * handle every combination of source and target password types.
151 : : */
2629 152 [ # # ]: 0 : elog(ERROR, "cannot encrypt password to requested type");
153 : : return NULL; /* keep compiler quiet */
154 : : }
155 : :
156 : : /*
157 : : * Check MD5 authentication response, and return STATUS_OK or STATUS_ERROR.
158 : : *
159 : : * 'shadow_pass' is the user's correct password or password hash, as stored
160 : : * in pg_authid.rolpassword.
161 : : * 'client_pass' is the response given by the remote user to the MD5 challenge.
162 : : * 'md5_salt' is the salt used in the MD5 authentication challenge.
163 : : *
164 : : * In the error case, save a string at *logdetail that will be sent to the
165 : : * postmaster log (but not the client).
166 : : */
167 : : int
2680 heikki.linnakangas@i 168 :CBC 1 : md5_crypt_verify(const char *role, const char *shadow_pass,
169 : : const char *client_pass,
170 : : const char *md5_salt, int md5_salt_len,
171 : : const char **logdetail)
172 : : {
173 : : int retval;
174 : : char crypt_pwd[MD5_PASSWD_LEN + 1];
824 michael@paquier.xyz 175 : 1 : const char *errstr = NULL;
176 : :
2680 heikki.linnakangas@i 177 [ - + ]: 1 : Assert(md5_salt_len > 0);
178 : :
2533 179 [ - + ]: 1 : if (get_password_type(shadow_pass) != PASSWORD_TYPE_MD5)
180 : : {
181 : : /* incompatible password hash format. */
2533 heikki.linnakangas@i 182 :UBC 0 : *logdetail = psprintf(_("User \"%s\" has a password that cannot be used with MD5 authentication."),
183 : : role);
184 : 0 : return STATUS_ERROR;
185 : : }
186 : :
187 : : /*
188 : : * Compute the correct answer for the MD5 challenge.
189 : : */
190 : : /* stored password already encrypted, only do salt */
2533 heikki.linnakangas@i 191 [ - + ]:CBC 1 : if (!pg_md5_encrypt(shadow_pass + strlen("md5"),
192 : : md5_salt, md5_salt_len,
193 : : crypt_pwd, &errstr))
194 : : {
824 michael@paquier.xyz 195 :UBC 0 : *logdetail = errstr;
2533 heikki.linnakangas@i 196 : 0 : return STATUS_ERROR;
197 : : }
198 : :
2680 heikki.linnakangas@i 199 [ + - ]:CBC 1 : if (strcmp(client_pass, crypt_pwd) == 0)
200 : 1 : retval = STATUS_OK;
201 : : else
202 : : {
2680 heikki.linnakangas@i 203 :UBC 0 : *logdetail = psprintf(_("Password does not match for user \"%s\"."),
204 : : role);
205 : 0 : retval = STATUS_ERROR;
206 : : }
207 : :
2680 heikki.linnakangas@i 208 :CBC 1 : return retval;
209 : : }
210 : :
211 : : /*
212 : : * Check given password for given user, and return STATUS_OK or STATUS_ERROR.
213 : : *
214 : : * 'shadow_pass' is the user's correct password hash, as stored in
215 : : * pg_authid.rolpassword.
216 : : * 'client_pass' is the password given by the remote user.
217 : : *
218 : : * In the error case, store a string at *logdetail that will be sent to the
219 : : * postmaster log (but not the client).
220 : : */
221 : : int
222 : 89 : plain_crypt_verify(const char *role, const char *shadow_pass,
223 : : const char *client_pass,
224 : : const char **logdetail)
225 : : {
226 : : char crypt_client_pass[MD5_PASSWD_LEN + 1];
824 michael@paquier.xyz 227 : 89 : const char *errstr = NULL;
228 : :
229 : : /*
230 : : * Client sent password in plaintext. If we have an MD5 hash stored, hash
231 : : * the password the client sent, and compare the hashes. Otherwise
232 : : * compare the plaintext passwords directly.
233 : : */
2629 heikki.linnakangas@i 234 [ + + + - ]: 89 : switch (get_password_type(shadow_pass))
235 : : {
2553 236 : 19 : case PASSWORD_TYPE_SCRAM_SHA_256:
2585 237 [ + + ]: 19 : if (scram_verify_plain_password(role,
238 : : client_pass,
239 : : shadow_pass))
240 : : {
241 : 12 : return STATUS_OK;
242 : : }
243 : : else
244 : : {
245 : 7 : *logdetail = psprintf(_("Password does not match for user \"%s\"."),
246 : : role);
247 : 7 : return STATUS_ERROR;
248 : : }
249 : : break;
250 : :
2629 251 : 13 : case PASSWORD_TYPE_MD5:
252 [ - + ]: 13 : if (!pg_md5_encrypt(client_pass,
253 : : role,
254 : : strlen(role),
255 : : crypt_client_pass,
256 : : &errstr))
257 : : {
824 michael@paquier.xyz 258 :UBC 0 : *logdetail = errstr;
2629 heikki.linnakangas@i 259 : 0 : return STATUS_ERROR;
260 : : }
2585 heikki.linnakangas@i 261 [ + + ]:CBC 13 : if (strcmp(crypt_client_pass, shadow_pass) == 0)
262 : 5 : return STATUS_OK;
263 : : else
264 : : {
265 : 8 : *logdetail = psprintf(_("Password does not match for user \"%s\"."),
266 : : role);
267 : 8 : return STATUS_ERROR;
268 : : }
269 : : break;
270 : :
2629 271 : 57 : case PASSWORD_TYPE_PLAINTEXT:
272 : :
273 : : /*
274 : : * We never store passwords in plaintext, so this shouldn't
275 : : * happen.
276 : : */
277 : 57 : break;
278 : : }
279 : :
280 : : /*
281 : : * This shouldn't happen. Plain "password" authentication is possible
282 : : * with any kind of stored password hash.
283 : : */
2585 284 : 57 : *logdetail = psprintf(_("Password of user \"%s\" is in unrecognized format."),
285 : : role);
286 : 57 : return STATUS_ERROR;
287 : : }
|