Add PQencryptPasswordConn function to libpq, use it in psql and createuser.

The new function supports creating SCRAM verifiers, in addition to md5
hashes. The algorithm is chosen based on password_encryption, by default.

This fixes the issue reported by Jeff Janes, that there was previously
no way to create a SCRAM verifier with "\password".

Michael Paquier and me

Discussion: https://www.postgresql.org/message-id/CAMkU%3D1wfBgFPbfAMYZQE78p%3DVhZX7nN86aWkp0QcCp%3D%2BKxZ%3Dbg%40mail.gmail.com
This commit is contained in:
Heikki Linnakangas 2017-05-03 11:19:07 +03:00
parent af2c5aa88d
commit 8f8b9be51f
13 changed files with 299 additions and 84 deletions

View File

@ -5875,6 +5875,58 @@ void PQconninfoFree(PQconninfoOption *connOptions);
</listitem>
</varlistentry>
<varlistentry id="libpq-pqencryptpasswordconn">
<term>
<function>PQencryptPasswordConn</function>
<indexterm>
<primary>PQencryptPasswordConn</primary>
</indexterm>
</term>
<listitem>
<para>
Prepares the encrypted form of a <productname>PostgreSQL</> password.
<synopsis>
char *PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user, const char *algorithm);
</synopsis>
This function is intended to be used by client applications that
wish to send commands like <literal>ALTER USER joe PASSWORD
'pwd'</>. It is good practice not to send the original cleartext
password in such a command, because it might be exposed in command
logs, activity displays, and so on. Instead, use this function to
convert the password to encrypted form before it is sent.
</para>
<para>
The <parameter>passwd</> and <parameter>user</> arguments
are the cleartext password, and the SQL name of the user it is for.
<parameter>algorithm</> specifies the encryption algorithm
to use to encrypt the password. Currently supported algorithms are
<literal>md5</>, <literal>scram-sha-256</> and <literal>plain</>.
<literal>scram-sha-256</> was introduced in <productname>PostgreSQL</>
version 10, and will not work correctly with older server versions. If
<parameter>algorithm</> is <symbol>NULL</>, this function will query
the server for the current value of the
<xref linkend="guc-password-encryption"> setting. That can block, and
will fail if the current transaction is aborted, or if the connection
is busy executing another query. If you wish to use the default
algorithm for the server but want to avoid blocking, query
<varname>password_encryption</> yourself before calling
<function>PQencryptPasswordConn</>, and pass that value as the
<parameter>algorithm</>.
</para>
<para>
The return value is a string allocated by <function>malloc</>.
The caller can assume the string doesn't contain any special characters
that would require escaping. Use <function>PQfreemem</> to free the
result when done with it. On error, returns <symbol>NULL</>, and
a suitable message is stored in the connection object.
</para>
</listitem>
</varlistentry>
<varlistentry id="libpq-pqencryptpassword">
<term>
<function>PQencryptPassword</function>
@ -5885,22 +5937,15 @@ void PQconninfoFree(PQconninfoOption *connOptions);
<listitem>
<para>
Prepares the encrypted form of a <productname>PostgreSQL</> password.
<synopsis>
char * PQencryptPassword(const char *passwd, const char *user);
</synopsis>
This function is intended to be used by client applications that
wish to send commands like <literal>ALTER USER joe PASSWORD
'pwd'</>. It is good practice not to send the original cleartext
password in such a command, because it might be exposed in command
logs, activity displays, and so on. Instead, use this function to
convert the password to encrypted form before it is sent. The
arguments are the cleartext password, and the SQL name of the user
it is for. The return value is a string allocated by
<function>malloc</function>, or <symbol>NULL</symbol> if out of
memory. The caller can assume the string doesn't contain any
special characters that would require escaping. Use
<function>PQfreemem</> to free the result when done with it.
Prepares the md5-encrypted form of a <productname>PostgreSQL</> password.
<synopsis>
char *PQencryptPassword(const char *passwd, const char *user);
</synopsis>
<function>PQencryptPassword</> is an older, deprecated version of
<function>PQencryptPasswodConn</>. The difference is that
<function>PQencryptPassword</> does not
require a connection object, and <literal>md5</> is always used as the
encryption algorithm.
</para>
</listitem>
</varlistentry>

View File

@ -207,7 +207,7 @@ pg_be_scram_init(const char *username, const char *shadow_pass)
*/
char *verifier;
verifier = scram_build_verifier(username, shadow_pass, 0);
verifier = pg_be_scram_build_verifier(shadow_pass);
(void) parse_scram_verifier(verifier, &state->iterations, &state->salt,
state->StoredKey, state->ServerKey);
@ -387,22 +387,14 @@ pg_be_scram_exchange(void *opaq, char *input, int inputlen,
/*
* Construct a verifier string for SCRAM, stored in pg_authid.rolpassword.
*
* If iterations is 0, default number of iterations is used. The result is
* palloc'd, so caller is responsible for freeing it.
* The result is palloc'd, so caller is responsible for freeing it.
*/
char *
scram_build_verifier(const char *username, const char *password,
int iterations)
pg_be_scram_build_verifier(const char *password)
{
char *prep_password = NULL;
pg_saslprep_rc rc;
char saltbuf[SCRAM_DEFAULT_SALT_LEN];
uint8 salted_password[SCRAM_KEY_LEN];
uint8 keybuf[SCRAM_KEY_LEN];
char *encoded_salt;
char *encoded_storedkey;
char *encoded_serverkey;
int encoded_len;
char *result;
/*
@ -414,10 +406,7 @@ scram_build_verifier(const char *username, const char *password,
if (rc == SASLPREP_SUCCESS)
password = (const char *) prep_password;
if (iterations <= 0)
iterations = SCRAM_DEFAULT_ITERATIONS;
/* Generate salt, and encode it in base64 */
/* Generate random salt */
if (!pg_backend_random(saltbuf, SCRAM_DEFAULT_SALT_LEN))
{
ereport(LOG,
@ -426,37 +415,11 @@ scram_build_verifier(const char *username, const char *password,
return NULL;
}
encoded_salt = palloc(pg_b64_enc_len(SCRAM_DEFAULT_SALT_LEN) + 1);
encoded_len = pg_b64_encode(saltbuf, SCRAM_DEFAULT_SALT_LEN, encoded_salt);
encoded_salt[encoded_len] = '\0';
/* Calculate StoredKey, and encode it in base64 */
scram_SaltedPassword(password, saltbuf, SCRAM_DEFAULT_SALT_LEN,
iterations, salted_password);
scram_ClientKey(salted_password, keybuf);
scram_H(keybuf, SCRAM_KEY_LEN, keybuf); /* StoredKey */
encoded_storedkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
encoded_storedkey);
encoded_storedkey[encoded_len] = '\0';
/* And same for ServerKey */
scram_ServerKey(salted_password, keybuf);
encoded_serverkey = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1);
encoded_len = pg_b64_encode((const char *) keybuf, SCRAM_KEY_LEN,
encoded_serverkey);
encoded_serverkey[encoded_len] = '\0';
result = psprintf("SCRAM-SHA-256$%d:%s$%s:%s", iterations, encoded_salt,
encoded_storedkey, encoded_serverkey);
result = scram_build_verifier(saltbuf, SCRAM_DEFAULT_SALT_LEN,
SCRAM_DEFAULT_ITERATIONS, password);
if (prep_password)
pfree(prep_password);
pfree(encoded_salt);
pfree(encoded_storedkey);
pfree(encoded_serverkey);
return result;
}
@ -1194,7 +1157,7 @@ scram_MockSalt(const char *username)
* Generate salt using a SHA256 hash of the username and the cluster's
* mock authentication nonce. (This works as long as the salt length is
* not larger the SHA256 digest length. If the salt is smaller, the caller
* will just ignore the extra data))
* will just ignore the extra data.)
*/
StaticAssertStmt(PG_SHA256_DIGEST_LENGTH >= SCRAM_DEFAULT_SALT_LEN,
"salt length greater than SHA256 digest length");

View File

@ -156,7 +156,7 @@ encrypt_password(PasswordType target_type, const char *role,
switch (guessed_type)
{
case PASSWORD_TYPE_PLAINTEXT:
return scram_build_verifier(role, password, 0);
return pg_be_scram_build_verifier(password);
case PASSWORD_TYPE_MD5:

View File

@ -1878,11 +1878,11 @@ exec_command_password(PsqlScanState scan_state, bool active_branch)
else
user = PQuser(pset.db);
encrypted_password = PQencryptPassword(pw1, user);
encrypted_password = PQencryptPasswordConn(pset.db, pw1, user, NULL);
if (!encrypted_password)
{
psql_error("Password encryption failed.\n");
psql_error("%s", PQerrorMessage(pset.db));
success = false;
}
else

View File

@ -274,11 +274,14 @@ main(int argc, char *argv[])
{
char *encrypted_password;
encrypted_password = PQencryptPassword(newpassword,
newuser);
encrypted_password = PQencryptPasswordConn(conn,
newpassword,
newuser,
NULL);
if (!encrypted_password)
{
fprintf(stderr, _("Password encryption failed.\n"));
fprintf(stderr, _("%s: password encryption failed: %s"),
progname, PQerrorMessage(conn));
exit(1);
}
appendStringLiteralConn(&sql, encrypted_password, conn);

View File

@ -23,6 +23,7 @@
#include <netinet/in.h>
#include <arpa/inet.h>
#include "common/base64.h"
#include "common/scram-common.h"
#define HMAC_IPAD 0x36
@ -180,3 +181,66 @@ scram_ServerKey(const uint8 *salted_password, uint8 *result)
scram_HMAC_update(&ctx, "Server Key", strlen("Server Key"));
scram_HMAC_final(result, &ctx);
}
/*
* Construct a verifier string for SCRAM, stored in pg_authid.rolpassword.
*
* The password should already have been processed with SASLprep, if necessary!
*
* If iterations is 0, default number of iterations is used. The result is
* palloc'd or malloc'd, so caller is responsible for freeing it.
*/
char *
scram_build_verifier(const char *salt, int saltlen, int iterations,
const char *password)
{
uint8 salted_password[SCRAM_KEY_LEN];
uint8 stored_key[SCRAM_KEY_LEN];
uint8 server_key[SCRAM_KEY_LEN];
char *result;
char *p;
int maxlen;
if (iterations <= 0)
iterations = SCRAM_DEFAULT_ITERATIONS;
/* Calculate StoredKey and ServerKey */
scram_SaltedPassword(password, salt, saltlen, iterations,
salted_password);
scram_ClientKey(salted_password, stored_key);
scram_H(stored_key, SCRAM_KEY_LEN, stored_key);
scram_ServerKey(salted_password, server_key);
/*
* The format is:
* SCRAM-SHA-256$<iteration count>:<salt>$<StoredKey>:<ServerKey>
*/
maxlen = strlen("SCRAM-SHA-256") + 1
+ 10 + 1 /* iteration count */
+ pg_b64_enc_len(saltlen) + 1 /* Base64-encoded salt */
+ pg_b64_enc_len(SCRAM_KEY_LEN) + 1 /* Base64-encoded StoredKey */
+ pg_b64_enc_len(SCRAM_KEY_LEN) + 1; /* Base64-encoded ServerKey */
#ifdef FRONTEND
result = malloc(maxlen);
if (!result)
return NULL;
#else
result = palloc(maxlen);
#endif
p = result + sprintf(result, "SCRAM-SHA-256$%d:", iterations);
p += pg_b64_encode(salt, saltlen, p);
*(p++) = '$';
p += pg_b64_encode((char *) stored_key, SCRAM_KEY_LEN, p);
*(p++) = ':';
p += pg_b64_encode((char *) server_key, SCRAM_KEY_LEN, p);
*(p++) = '\0';
Assert(p - result <= maxlen);
return result;
}

View File

@ -53,4 +53,7 @@ extern void scram_H(const uint8 *str, int len, uint8 *result);
extern void scram_ClientKey(const uint8 *salted_password, uint8 *result);
extern void scram_ServerKey(const uint8 *salted_password, uint8 *result);
extern char *scram_build_verifier(const char *salt, int saltlen, int iterations,
const char *password);
#endif /* SCRAM_COMMON_H */

View File

@ -27,9 +27,7 @@ extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
/* Routines to handle and check SCRAM-SHA-256 verifier */
extern char *scram_build_verifier(const char *username,
const char *password,
int iterations);
extern char *pg_be_scram_build_verifier(const char *password);
extern bool is_scram_verifier(const char *verifier);
extern bool scram_verify_plain_password(const char *username,
const char *password, const char *verifier);

View File

@ -171,3 +171,4 @@ PQsslAttributeNames 168
PQsslAttribute 169
PQsetErrorContextVisibility 170
PQresultVerboseErrorMessage 171
PQencryptPasswordConn 172

View File

@ -614,6 +614,41 @@ verify_server_signature(fe_scram_state *state)
return true;
}
/*
* Build a new SCRAM verifier.
*/
char *
pg_fe_scram_build_verifier(const char *password)
{
char *prep_password = NULL;
pg_saslprep_rc rc;
char saltbuf[SCRAM_DEFAULT_SALT_LEN];
char *result;
/*
* Normalize the password with SASLprep. If that doesn't work, because
* the password isn't valid UTF-8 or contains prohibited characters, just
* proceed with the original password. (See comments at top of file.)
*/
rc = pg_saslprep(password, &prep_password);
if (rc == SASLPREP_OOM)
return NULL;
if (rc == SASLPREP_SUCCESS)
password = (const char *) prep_password;
/* Generate a random salt */
if (!pg_frontend_random(saltbuf, SCRAM_DEFAULT_SALT_LEN))
return NULL;
result = scram_build_verifier(saltbuf, SCRAM_DEFAULT_SALT_LEN,
SCRAM_DEFAULT_ITERATIONS, password);
if (prep_password)
free(prep_password);
return result;
}
/*
* Random number generator.
*/

View File

@ -1077,22 +1077,12 @@ pg_fe_getauthname(PQExpBuffer errorMessage)
/*
* PQencryptPassword -- exported routine to encrypt a password
* PQencryptPassword -- exported routine to encrypt a password with MD5
*
* This is intended to be used by client applications that wish to send
* commands like ALTER USER joe PASSWORD 'pwd'. The password need not
* be sent in cleartext if it is encrypted on the client side. This is
* good because it ensures the cleartext password won't end up in logs,
* pg_stat displays, etc. We export the function so that clients won't
* be dependent on low-level details like whether the encryption is MD5
* or something else.
*
* Arguments are the cleartext password, and the SQL name of the user it
* is for.
*
* Return value is a malloc'd string, or NULL if out-of-memory. The client
* may assume the string doesn't contain any special characters that would
* require escaping.
* This function is equivalent to calling PQencryptPasswordConn with
* "md5" as the encryption method, except that this doesn't require
* a connection object. This function is deprecated, use
* PQencryptPasswordConn instead.
*/
char *
PQencryptPassword(const char *passwd, const char *user)
@ -1111,3 +1101,114 @@ PQencryptPassword(const char *passwd, const char *user)
return crypt_pwd;
}
/*
* PQencryptPasswordConn -- exported routine to encrypt a password
*
* This is intended to be used by client applications that wish to send
* commands like ALTER USER joe PASSWORD 'pwd'. The password need not
* be sent in cleartext if it is encrypted on the client side. This is
* good because it ensures the cleartext password won't end up in logs,
* pg_stat displays, etc. We export the function so that clients won't
* be dependent on low-level details like whether the encryption is MD5
* or something else.
*
* Arguments are a connection object, the cleartext password, the SQL
* name of the user it is for, and a string indicating the algorithm to
* use for encrypting the password. If algorithm is NULL, this queries
* the server for the current 'password_encryption' value. If you wish
* to avoid that, e.g. to avoid blocking, you can execute
* 'show password_encryption' yourself before calling this function, and
* pass it as the algorithm.
*
* Return value is a malloc'd string. The client may assume the string
* doesn't contain any special characters that would require escaping.
* On error, an error message is stored in the connection object, and
* returns NULL.
*/
char *
PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user,
const char *algorithm)
{
#define MAX_ALGORITHM_NAME_LEN 50
char algobuf[MAX_ALGORITHM_NAME_LEN + 1];
char *crypt_pwd = NULL;
if (!conn)
return NULL;
/* If no algorithm was given, ask the server. */
if (algorithm == NULL)
{
PGresult *res;
char *val;
res = PQexec(conn, "show password_encryption");
if (res == NULL)
{
/* PQexec() should've set conn->errorMessage already */
return NULL;
}
if (PQresultStatus(res) != PGRES_TUPLES_OK)
{
/* PQexec() should've set conn->errorMessage already */
PQclear(res);
return NULL;
}
if (PQntuples(res) != 1 || PQnfields(res) != 1)
{
PQclear(res);
printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("unexpected shape of result set returned for SHOW\n"));
return NULL;
}
val = PQgetvalue(res, 0, 0);
if (strlen(val) > MAX_ALGORITHM_NAME_LEN)
{
PQclear(res);
printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("password_encryption value too long\n"));
return NULL;
}
strcpy(algobuf, val);
PQclear(res);
algorithm = algobuf;
}
/* Ok, now we know what algorithm to use */
if (strcmp(algorithm, "scram-sha-256") == 0)
{
crypt_pwd = pg_fe_scram_build_verifier(passwd);
}
else if (strcmp(algorithm, "md5") == 0)
{
crypt_pwd = malloc(MD5_PASSWD_LEN + 1);
if (crypt_pwd)
{
if (!pg_md5_encrypt(passwd, user, strlen(user), crypt_pwd))
{
free(crypt_pwd);
crypt_pwd = NULL;
}
}
}
else if (strcmp(algorithm, "plain") == 0)
{
crypt_pwd = strdup(passwd);
}
else
{
printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("unknown password encryption algorithm\n"));
return NULL;
}
if (!crypt_pwd)
printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("out of memory\n"));
return crypt_pwd;
}

View File

@ -28,5 +28,6 @@ extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
bool *done, bool *success, PQExpBuffer errorMessage);
extern char *pg_fe_scram_build_verifier(const char *password);
#endif /* FE_AUTH_H */

View File

@ -597,6 +597,7 @@ extern int PQenv2encoding(void);
/* === in fe-auth.c === */
extern char *PQencryptPassword(const char *passwd, const char *user);
extern char *PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user, const char *algorithm);
/* === in encnames.c === */