1/*-------------------------------------------------------------------------
2 *
3 * crypt.c
4 * Functions for dealing with encrypted passwords stored in
5 * pg_authid.rolpassword.
6 *
7 * Portions Copyright (c) 1996-2019, 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#ifdef HAVE_CRYPT_H
18#include <crypt.h>
19#endif
20
21#include "catalog/pg_authid.h"
22#include "common/md5.h"
23#include "common/scram-common.h"
24#include "libpq/crypt.h"
25#include "libpq/scram.h"
26#include "miscadmin.h"
27#include "utils/builtins.h"
28#include "utils/syscache.h"
29#include "utils/timestamp.h"
30
31
32/*
33 * Fetch stored password for a user, for authentication.
34 *
35 * On error, returns NULL, and stores a palloc'd string describing the reason,
36 * for the postmaster log, in *logdetail. The error reason should *not* be
37 * sent to the client, to avoid giving away user information!
38 */
39char *
40get_role_password(const char *role, char **logdetail)
41{
42 TimestampTz vuntil = 0;
43 HeapTuple roleTup;
44 Datum datum;
45 bool isnull;
46 char *shadow_pass;
47
48 /* Get role info from pg_authid */
49 roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(role));
50 if (!HeapTupleIsValid(roleTup))
51 {
52 *logdetail = psprintf(_("Role \"%s\" does not exist."),
53 role);
54 return NULL; /* no such user */
55 }
56
57 datum = SysCacheGetAttr(AUTHNAME, roleTup,
58 Anum_pg_authid_rolpassword, &isnull);
59 if (isnull)
60 {
61 ReleaseSysCache(roleTup);
62 *logdetail = psprintf(_("User \"%s\" has no password assigned."),
63 role);
64 return NULL; /* user has no password */
65 }
66 shadow_pass = TextDatumGetCString(datum);
67
68 datum = SysCacheGetAttr(AUTHNAME, roleTup,
69 Anum_pg_authid_rolvaliduntil, &isnull);
70 if (!isnull)
71 vuntil = DatumGetTimestampTz(datum);
72
73 ReleaseSysCache(roleTup);
74
75 /*
76 * Password OK, but check to be sure we are not past rolvaliduntil
77 */
78 if (!isnull && vuntil < GetCurrentTimestamp())
79 {
80 *logdetail = psprintf(_("User \"%s\" has an expired password."),
81 role);
82 return NULL;
83 }
84
85 return shadow_pass;
86}
87
88/*
89 * What kind of a password verifier is 'shadow_pass'?
90 */
91PasswordType
92get_password_type(const char *shadow_pass)
93{
94 char *encoded_salt;
95 int iterations;
96 uint8 stored_key[SCRAM_KEY_LEN];
97 uint8 server_key[SCRAM_KEY_LEN];
98
99 if (strncmp(shadow_pass, "md5", 3) == 0 &&
100 strlen(shadow_pass) == MD5_PASSWD_LEN &&
101 strspn(shadow_pass + 3, MD5_PASSWD_CHARSET) == MD5_PASSWD_LEN - 3)
102 return PASSWORD_TYPE_MD5;
103 if (parse_scram_verifier(shadow_pass, &iterations, &encoded_salt,
104 stored_key, server_key))
105 return PASSWORD_TYPE_SCRAM_SHA_256;
106 return PASSWORD_TYPE_PLAINTEXT;
107}
108
109/*
110 * Given a user-supplied password, convert it into a verifier 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 */
116char *
117encrypt_password(PasswordType target_type, const char *role,
118 const char *password)
119{
120 PasswordType guessed_type = get_password_type(password);
121 char *encrypted_password;
122
123 if (guessed_type != PASSWORD_TYPE_PLAINTEXT)
124 {
125 /*
126 * Cannot convert an already-encrypted password from one format to
127 * another, so return it as it is.
128 */
129 return pstrdup(password);
130 }
131
132 switch (target_type)
133 {
134 case PASSWORD_TYPE_MD5:
135 encrypted_password = palloc(MD5_PASSWD_LEN + 1);
136
137 if (!pg_md5_encrypt(password, role, strlen(role),
138 encrypted_password))
139 elog(ERROR, "password encryption failed");
140 return encrypted_password;
141
142 case PASSWORD_TYPE_SCRAM_SHA_256:
143 return pg_be_scram_build_verifier(password);
144
145 case PASSWORD_TYPE_PLAINTEXT:
146 elog(ERROR, "cannot encrypt password with 'plaintext'");
147 }
148
149 /*
150 * This shouldn't happen, because the above switch statements should
151 * handle every combination of source and target password types.
152 */
153 elog(ERROR, "cannot encrypt password to requested type");
154 return NULL; /* keep compiler quiet */
155}
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, optionally store a palloc'd string at *logdetail
166 * that will be sent to the postmaster log (but not the client).
167 */
168int
169md5_crypt_verify(const char *role, const char *shadow_pass,
170 const char *client_pass,
171 const char *md5_salt, int md5_salt_len,
172 char **logdetail)
173{
174 int retval;
175 char crypt_pwd[MD5_PASSWD_LEN + 1];
176
177 Assert(md5_salt_len > 0);
178
179 if (get_password_type(shadow_pass) != PASSWORD_TYPE_MD5)
180 {
181 /* incompatible password hash format. */
182 *logdetail = psprintf(_("User \"%s\" has a password that cannot be used with MD5 authentication."),
183 role);
184 return STATUS_ERROR;
185 }
186
187 /*
188 * Compute the correct answer for the MD5 challenge.
189 *
190 * We do not bother setting logdetail for any pg_md5_encrypt failure
191 * below: the only possible error is out-of-memory, which is unlikely, and
192 * if it did happen adding a psprintf call would only make things worse.
193 */
194 /* stored password already encrypted, only do salt */
195 if (!pg_md5_encrypt(shadow_pass + strlen("md5"),
196 md5_salt, md5_salt_len,
197 crypt_pwd))
198 {
199 return STATUS_ERROR;
200 }
201
202 if (strcmp(client_pass, crypt_pwd) == 0)
203 retval = STATUS_OK;
204 else
205 {
206 *logdetail = psprintf(_("Password does not match for user \"%s\"."),
207 role);
208 retval = STATUS_ERROR;
209 }
210
211 return retval;
212}
213
214/*
215 * Check given password for given user, and return STATUS_OK or STATUS_ERROR.
216 *
217 * 'shadow_pass' is the user's correct password hash, as stored in
218 * pg_authid.rolpassword.
219 * 'client_pass' is the password given by the remote user.
220 *
221 * In the error case, optionally store a palloc'd string at *logdetail
222 * that will be sent to the postmaster log (but not the client).
223 */
224int
225plain_crypt_verify(const char *role, const char *shadow_pass,
226 const char *client_pass,
227 char **logdetail)
228{
229 char crypt_client_pass[MD5_PASSWD_LEN + 1];
230
231 /*
232 * Client sent password in plaintext. If we have an MD5 hash stored, hash
233 * the password the client sent, and compare the hashes. Otherwise
234 * compare the plaintext passwords directly.
235 */
236 switch (get_password_type(shadow_pass))
237 {
238 case PASSWORD_TYPE_SCRAM_SHA_256:
239 if (scram_verify_plain_password(role,
240 client_pass,
241 shadow_pass))
242 {
243 return STATUS_OK;
244 }
245 else
246 {
247 *logdetail = psprintf(_("Password does not match for user \"%s\"."),
248 role);
249 return STATUS_ERROR;
250 }
251 break;
252
253 case PASSWORD_TYPE_MD5:
254 if (!pg_md5_encrypt(client_pass,
255 role,
256 strlen(role),
257 crypt_client_pass))
258 {
259 /*
260 * We do not bother setting logdetail for pg_md5_encrypt
261 * failure: the only possible error is out-of-memory, which is
262 * unlikely, and if it did happen adding a psprintf call would
263 * only make things worse.
264 */
265 return STATUS_ERROR;
266 }
267 if (strcmp(crypt_client_pass, shadow_pass) == 0)
268 return STATUS_OK;
269 else
270 {
271 *logdetail = psprintf(_("Password does not match for user \"%s\"."),
272 role);
273 return STATUS_ERROR;
274 }
275 break;
276
277 case PASSWORD_TYPE_PLAINTEXT:
278
279 /*
280 * We never store passwords in plaintext, so this shouldn't
281 * happen.
282 */
283 break;
284 }
285
286 /*
287 * This shouldn't happen. Plain "password" authentication is possible
288 * with any kind of stored password hash.
289 */
290 *logdetail = psprintf(_("Password of user \"%s\" is in unrecognized format."),
291 role);
292 return STATUS_ERROR;
293}
294