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